diff options
| author | Kamil Trzciński <ayufan@ayufan.eu> | 2018-02-28 20:36:55 +0100 |
|---|---|---|
| committer | Kamil Trzciński <ayufan@ayufan.eu> | 2018-02-28 20:36:55 +0100 |
| commit | 965dc28691e2d70b7040e28d90ccbc3721a9e416 (patch) | |
| tree | 84258f35b72f2e7ce6a7198db66032df4ad5aadb /spec/javascripts | |
| parent | e3fafa7632e038927085cf8c8228c93be44b36bd (diff) | |
| parent | 7fabc892f251740dbd9a4755baede662e6854870 (diff) | |
| download | gitlab-ce-965dc28691e2d70b7040e28d90ccbc3721a9e416.tar.gz | |
Merge commit '7fabc892f251740dbd9a4755baede662e6854870' into object-storage-ee-to-ce-backport
Diffstat (limited to 'spec/javascripts')
114 files changed, 4706 insertions, 1019 deletions
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index 67afba19190..960b731892a 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -1,21 +1,18 @@ -/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */ - import '~/behaviors/autosize'; -(function() { - describe('Autosize behavior', function() { - var load; - beforeEach(function() { - return setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>'); - }); - it('does not overwrite the resize property', function() { - load(); - return expect($('textarea')).toHaveCss({ - resize: 'vertical' - }); +function load() { + $(document).trigger('load'); +} + +describe('Autosize behavior', () => { + beforeEach(() => { + setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>'); + }); + + it('does not overwrite the resize property', () => { + load(); + expect($('textarea')).toHaveCss({ + resize: 'vertical', }); - return load = function() { - return $(document).trigger('load'); - }; }); -}).call(window); +}); diff --git a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js index ec2c549e032..f96f20ed4a5 100644 --- a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js +++ b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js @@ -21,13 +21,18 @@ describe('Unicode Support Map', () => { }); it('should call .getItem and .setItem', () => { - const allArgs = window.localStorage.setItem.calls.allArgs(); - - expect(window.localStorage.getItem).toHaveBeenCalledWith('gl-emoji-user-agent'); - expect(allArgs[0][0]).toBe('gl-emoji-user-agent'); - expect(allArgs[0][1]).toBe(navigator.userAgent); - expect(allArgs[1][0]).toBe('gl-emoji-unicode-support-map'); - expect(allArgs[1][1]).toBe(stringSupportMap); + const getArgs = window.localStorage.getItem.calls.allArgs(); + const setArgs = window.localStorage.setItem.calls.allArgs(); + + expect(getArgs[0][0]).toBe('gl-emoji-version'); + expect(getArgs[1][0]).toBe('gl-emoji-user-agent'); + + expect(setArgs[0][0]).toBe('gl-emoji-version'); + expect(setArgs[0][1]).toBe('0.2.0'); + expect(setArgs[1][0]).toBe('gl-emoji-user-agent'); + expect(setArgs[1][1]).toBe(navigator.userAgent); + expect(setArgs[2][0]).toBe('gl-emoji-unicode-support-map'); + expect(setArgs[2][1]).toBe(stringSupportMap); }); }); diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index f9fa814b801..8287c58ac5a 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -1,39 +1,43 @@ -/* eslint-disable space-before-function-paren, no-var */ - import '~/behaviors/requires_input'; -(function() { - describe('requiresInput', function() { - preloadFixtures('branches/new_branch.html.raw'); - beforeEach(function() { - loadFixtures('branches/new_branch.html.raw'); - this.submitButton = $('button[type="submit"]'); - }); - it('disables submit when any field is required', function() { - $('.js-requires-input').requiresInput(); - return expect(this.submitButton).toBeDisabled(); - }); - it('enables submit when no field is required', function() { - $('*[required=required]').removeAttr('required'); - $('.js-requires-input').requiresInput(); - return expect(this.submitButton).not.toBeDisabled(); - }); - it('enables submit when all required fields are pre-filled', function() { - $('*[required=required]').remove(); - $('.js-requires-input').requiresInput(); - return expect($('.submit')).not.toBeDisabled(); - }); - it('enables submit when all required fields receive input', function() { - $('.js-requires-input').requiresInput(); - $('#required1').val('input1').change(); - expect(this.submitButton).toBeDisabled(); - $('#optional1').val('input1').change(); - expect(this.submitButton).toBeDisabled(); - $('#required2').val('input2').change(); - $('#required3').val('input3').change(); - $('#required4').val('input4').change(); - $('#required5').val('1').change(); - return expect($('.submit')).not.toBeDisabled(); - }); +describe('requiresInput', () => { + let submitButton; + preloadFixtures('branches/new_branch.html.raw'); + + beforeEach(() => { + loadFixtures('branches/new_branch.html.raw'); + submitButton = $('button[type="submit"]'); + }); + + it('disables submit when any field is required', () => { + $('.js-requires-input').requiresInput(); + expect(submitButton).toBeDisabled(); + }); + + it('enables submit when no field is required', () => { + $('*[required=required]').removeAttr('required'); + $('.js-requires-input').requiresInput(); + expect(submitButton).not.toBeDisabled(); + }); + + it('enables submit when all required fields are pre-filled', () => { + $('*[required=required]').remove(); + $('.js-requires-input').requiresInput(); + expect($('.submit')).not.toBeDisabled(); + }); + + it('enables submit when all required fields receive input', () => { + $('.js-requires-input').requiresInput(); + $('#required1').val('input1').change(); + expect(submitButton).toBeDisabled(); + + $('#optional1').val('input1').change(); + expect(submitButton).toBeDisabled(); + + $('#required2').val('input2').change(); + $('#required3').val('input3').change(); + $('#required4').val('input4').change(); + $('#required5').val('1').change(); + expect($('.submit')).not.toBeDisabled(); }); -}).call(window); +}); diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 83b13b06dc1..8f607899b20 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -9,10 +9,11 @@ import Vue from 'vue'; import '~/boards/models/assignee'; +import eventHub from '~/boards/eventhub'; import '~/boards/models/list'; import '~/boards/models/label'; import '~/boards/stores/boards_store'; -import boardCard from '~/boards/components/board_card'; +import boardCard from '~/boards/components/board_card.vue'; import './mock_data'; describe('Board card', () => { @@ -157,33 +158,35 @@ describe('Board card', () => { }); it('sets detail issue to card issue on mouse up', () => { + spyOn(eventHub, '$emit'); + triggerEvent('mousedown'); triggerEvent('mouseup'); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue); + expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue); expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list); }); it('adds active class if detail issue is set', (done) => { - triggerEvent('mousedown'); - triggerEvent('mouseup'); - - setTimeout(() => { - expect(vm.$el.classList.contains('is-active')).toBe(true); - done(); - }, 0); + vm.detailIssue.issue = vm.issue; + + Vue.nextTick() + .then(() => { + expect(vm.$el.classList.contains('is-active')).toBe(true); + }) + .then(done) + .catch(done.fail); }); it('resets detail issue to empty if already set', () => { - triggerEvent('mousedown'); - triggerEvent('mouseup'); + spyOn(eventHub, '$emit'); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue); + gl.issueBoards.BoardsStore.detail.issue = vm.issue; triggerEvent('mousedown'); triggerEvent('mouseup'); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue'); }); }); }); diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 022d286d5df..10b88878c2a 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -133,6 +133,25 @@ describe('Issue model', () => { expect(relativePositionIssue.position).toBe(1); }); + it('updates data', () => { + issue.updateData({ subscribed: true }); + expect(issue.subscribed).toBe(true); + }); + + it('sets fetching state', () => { + expect(issue.isFetching.subscriptions).toBe(true); + + issue.setFetchingState('subscriptions', false); + + expect(issue.isFetching.subscriptions).toBe(false); + }); + + it('sets loading state', () => { + issue.setLoadingState('foo', true); + + expect(issue.isLoading.foo).toBe(true); + }); + describe('update', () => { it('passes assignee ids when there are assignees', (done) => { spyOn(Vue.http, 'patch').and.callFake((url, data) => { diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index 027e8001053..f5be9ea0fb2 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -28,7 +28,7 @@ describe('Clusters', () => { expect( cluster.toggleButton.classList, - ).not.toContain('checked'); + ).not.toContain('is-checked'); expect( cluster.toggleInput.getAttribute('value'), @@ -36,6 +36,20 @@ describe('Clusters', () => { }); }); + describe('showToken', () => { + it('should update tye field type', () => { + cluster.showTokenButton.click(); + expect( + cluster.tokenField.getAttribute('type'), + ).toEqual('text'); + + cluster.showTokenButton.click(); + expect( + cluster.tokenField.getAttribute('type'), + ).toEqual('password'); + }); + }); + describe('checkForNewInstalls', () => { const INITIAL_APP_MAP = { helm: { status: null, title: 'Helm Tiller' }, @@ -113,7 +127,7 @@ describe('Clusters', () => { }); describe('when cluster is created', () => { - it('should show the success container', () => { + it('should show the success container and fresh the page', () => { cluster.updateContainer(null, 'created'); expect( diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js new file mode 100644 index 00000000000..0a8b63ed5b4 --- /dev/null +++ b/spec/javascripts/clusters/clusters_index_spec.js @@ -0,0 +1,58 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import setClusterTableToggles from '~/clusters/clusters_index'; +import { setTimeout } from 'core-js/library/web/timers'; + +describe('Clusters table', () => { + preloadFixtures('clusters/index_cluster.html.raw'); + let mock; + + beforeEach(() => { + loadFixtures('clusters/index_cluster.html.raw'); + mock = new MockAdapter(axios); + setClusterTableToggles(); + }); + + describe('update cluster', () => { + it('renders loading state while request is made', () => { + const button = document.querySelector('.js-toggle-cluster-list'); + + button.click(); + + expect(button.classList).toContain('is-loading'); + expect(button.getAttribute('disabled')).toEqual('true'); + }); + + afterEach(() => { + mock.restore(); + }); + + it('shows updated state after sucessfull request', (done) => { + mock.onPut().reply(200, {}, {}); + const button = document.querySelector('.js-toggle-cluster-list'); + button.click(); + + expect(button.classList).toContain('is-loading'); + + setTimeout(() => { + expect(button.classList).not.toContain('is-loading'); + expect(button.classList).not.toContain('is-checked'); + done(); + }, 0); + }); + + it('shows inital state after failed request', (done) => { + mock.onPut().reply(500, {}, {}); + const button = document.querySelector('.js-toggle-cluster-list'); + + button.click(); + expect(button.classList).toContain('is-loading'); + + setTimeout(() => { + expect(button.classList).not.toContain('is-loading'); + expect(button.classList).toContain('is-checked'); + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index 3391cade541..0f7bf9ec712 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -1,4 +1,4 @@ -import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; +import * as datetimeUtility from '~/lib/utils/datetime_utility'; (() => { describe('Date time utils', () => { @@ -89,10 +89,22 @@ import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; describe('timeIntervalInWords', () => { it('should return string with number of minutes and seconds', () => { - expect(timeIntervalInWords(9.54)).toEqual('9 seconds'); - expect(timeIntervalInWords(1)).toEqual('1 second'); - expect(timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); - expect(timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds'); + expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second'); + expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); + expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + }); + }); + + describe('dateInWords', () => { + const date = new Date('07/01/2016'); + + it('should return date in words', () => { + expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016'); + }); + + it('should return abbreviated month name', () => { + expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016'); }); }); })(); diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js index 5b93fbc5575..7025c3d836c 100644 --- a/spec/javascripts/deploy_keys/components/action_btn_spec.js +++ b/spec/javascripts/deploy_keys/components/action_btn_spec.js @@ -34,7 +34,7 @@ describe('Deploy keys action btn', () => { setTimeout(() => { expect( eventHub.$emit, - ).toHaveBeenCalledWith('enable.key', deployKey); + ).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything()); done(); }); diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js index 700897f50b0..0ca9290d3d2 100644 --- a/spec/javascripts/deploy_keys/components/app_spec.js +++ b/spec/javascripts/deploy_keys/components/app_spec.js @@ -139,4 +139,18 @@ describe('Deploy keys app component', () => { it('hasKeys returns true when there are keys', () => { expect(vm.hasKeys).toEqual(3); }); + + it('resets remove button loading state', (done) => { + spyOn(window, 'confirm').and.returnValue(false); + + const btn = vm.$el.querySelector('.btn-warning'); + + btn.click(); + + Vue.nextTick(() => { + expect(btn.querySelector('.fa')).toBeNull(); + + done(); + }); + }); }); diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js index 1ef494a00b8..1225fe2cb66 100644 --- a/spec/javascripts/droplab/drop_down_spec.js +++ b/spec/javascripts/droplab/drop_down_spec.js @@ -279,7 +279,12 @@ describe('DropDown', function () { describe('addEvents', function () { beforeEach(function () { this.list = { addEventListener: () => {} }; - this.dropdown = { list: this.list, clickEvent: () => {}, eventWrapper: {} }; + this.dropdown = { + list: this.list, + clickEvent: () => {}, + closeDropdown: () => {}, + eventWrapper: {}, + }; spyOn(this.list, 'addEventListener'); @@ -288,6 +293,7 @@ describe('DropDown', function () { it('should call .addEventListener', function () { expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function)); + expect(this.list.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function)); }); }); diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js index 75bf5f3d611..3d39bd0812b 100644 --- a/spec/javascripts/droplab/hook_spec.js +++ b/spec/javascripts/droplab/hook_spec.js @@ -24,7 +24,7 @@ describe('Hook', function () { }); it('should call DropDown constructor', function () { - expect(dropdownSrc.default).toHaveBeenCalledWith(this.list); + expect(dropdownSrc.default).toHaveBeenCalledWith(this.list, this.config); }); it('should set .type', function () { diff --git a/spec/javascripts/emoji_spec.js b/spec/javascripts/emoji_spec.js index fa11c602ec3..124d91f4477 100644 --- a/spec/javascripts/emoji_spec.js +++ b/spec/javascripts/emoji_spec.js @@ -1,6 +1,7 @@ import { glEmojiTag } from '~/emoji'; import isEmojiUnicodeSupported, { isFlagEmoji, + isRainbowFlagEmoji, isKeycapEmoji, isSkinToneComboEmoji, isHorceRacingSkinToneComboEmoji, @@ -217,6 +218,24 @@ describe('gl_emoji', () => { }); }); + describe('isRainbowFlagEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isRainbowFlagEmoji('')).toBeFalsy(); + }); + it('should detect rainbow_flag', () => { + expect(isRainbowFlagEmoji('🏳🌈')).toBeTruthy(); + }); + it('should not detect flag_white on its\' own', () => { + expect(isRainbowFlagEmoji('🏳')).toBeFalsy(); + }); + it('should not detect rainbow on its\' own', () => { + expect(isRainbowFlagEmoji('🌈')).toBeFalsy(); + }); + it('should not detect flag_white with something else', () => { + expect(isRainbowFlagEmoji('🏳🔵')).toBeFalsy(); + }); + }); + describe('isKeycapEmoji', () => { it('should gracefully handle empty string', () => { expect(isKeycapEmoji('')).toBeFalsy(); diff --git a/spec/javascripts/environments/emtpy_state_spec.js b/spec/javascripts/environments/emtpy_state_spec.js new file mode 100644 index 00000000000..82de35933f5 --- /dev/null +++ b/spec/javascripts/environments/emtpy_state_spec.js @@ -0,0 +1,57 @@ + +import Vue from 'vue'; +import emptyState from '~/environments/components/empty_state.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('environments empty state', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(emptyState); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('With permissions', () => { + beforeEach(() => { + vm = mountComponent(Component, { + newPath: 'foo', + canCreateEnvironment: true, + helpPath: 'bar', + }); + }); + + it('renders empty state and new environment button', () => { + expect( + vm.$el.querySelector('.js-blank-state-title').textContent.trim(), + ).toEqual('You don\'t have any environments right now.'); + + expect( + vm.$el.querySelector('.js-new-environment-button').getAttribute('href'), + ).toEqual('foo'); + }); + }); + + describe('Without permission', () => { + beforeEach(() => { + vm = mountComponent(Component, { + newPath: 'foo', + canCreateEnvironment: false, + helpPath: 'bar', + }); + }); + + it('renders empty state without new button', () => { + expect( + vm.$el.querySelector('.js-blank-state-title').textContent.trim(), + ).toEqual('You don\'t have any environments right now.'); + + expect( + vm.$el.querySelector('.js-new-environment-button'), + ).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js index 2862971bec4..9bd42863759 100644 --- a/spec/javascripts/environments/environment_table_spec.js +++ b/spec/javascripts/environments/environment_table_spec.js @@ -1,10 +1,17 @@ import Vue from 'vue'; import environmentTableComp from '~/environments/components/environments_table.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Environment table', () => { + let Component; + let vm; -describe('Environment item', () => { - preloadFixtures('static/environments/element.html.raw'); beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + Component = Vue.extend(environmentTableComp); + }); + + afterEach(() => { + vm.$destroy(); }); it('Should render a table', () => { @@ -17,18 +24,12 @@ describe('Environment item', () => { }, }; - const EnvironmentTable = Vue.extend(environmentTableComp); - - const component = new EnvironmentTable({ - el: document.querySelector('.test-dom-element'), - propsData: { - environments: [{ mockItem }], - canCreateDeployment: false, - canReadEnvironment: true, - service: {}, - }, - }).$mount(); + vm = mountComponent(Component, { + environments: [mockItem], + canCreateDeployment: false, + canReadEnvironment: true, + }); - expect(component.$el.getAttribute('class')).toContain('ci-table'); + expect(vm.$el.getAttribute('class')).toContain('ci-table'); }); }); diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environments_app_spec.js index 0c8817a8148..d02adb25b4e 100644 --- a/spec/javascripts/environments/environment_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -1,18 +1,24 @@ import Vue from 'vue'; -import '~/flash'; -import environmentsComponent from '~/environments/components/environment.vue'; +import environmentsComponent from '~/environments/components/environments_app.vue'; import { environment, folder } from './mock_data'; import { headersInterceptor } from '../helpers/vue_resource_helper'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Environment', () => { - preloadFixtures('static/environments/environments.html.raw'); + const mockData = { + endpoint: 'environments.json', + canCreateEnvironment: true, + canCreateDeployment: true, + canReadEnvironment: true, + cssContainerClass: 'container', + newEnvironmentPath: 'environments/new', + helpPagePath: 'help', + }; let EnvironmentsComponent; let component; beforeEach(() => { - loadFixtures('static/environments/environments.html.raw'); - EnvironmentsComponent = Vue.extend(environmentsComponent); }); @@ -37,9 +43,7 @@ describe('Environment', () => { }); it('should render the empty state', (done) => { - component = new EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - }); + component = mountComponent(EnvironmentsComponent, mockData); setTimeout(() => { expect( @@ -81,9 +85,7 @@ describe('Environment', () => { beforeEach(() => { Vue.http.interceptors.push(environmentsResponseInterceptor); Vue.http.interceptors.push(headersInterceptor); - component = new EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - }); + component = mountComponent(EnvironmentsComponent, mockData); }); afterEach(() => { @@ -95,7 +97,7 @@ describe('Environment', () => { it('should render a table with environments', (done) => { setTimeout(() => { - expect(component.$el.querySelectorAll('table')).toBeDefined(); + expect(component.$el.querySelectorAll('table')).not.toBeNull(); expect( component.$el.querySelector('.environment-name').textContent.trim(), ).toEqual(environment.name); @@ -104,10 +106,6 @@ describe('Environment', () => { }); describe('pagination', () => { - afterEach(() => { - window.history.pushState({}, null, ''); - }); - it('should render pagination', (done) => { setTimeout(() => { expect( @@ -117,46 +115,23 @@ describe('Environment', () => { }, 0); }); - it('should update url when no search params are present', (done) => { - spyOn(gl.utils, 'visitUrl'); + it('should make an API request when page is clicked', (done) => { + spyOn(component, 'updateContent'); setTimeout(() => { component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); - expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' }); done(); }, 0); }); - it('should update url when page is already present', (done) => { - spyOn(gl.utils, 'visitUrl'); - window.history.pushState({}, null, '?page=1'); - - setTimeout(() => { - component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); - expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); - done(); - }, 0); - }); - - it('should update url when page and scope are already present', (done) => { - spyOn(gl.utils, 'visitUrl'); - window.history.pushState({}, null, '?scope=all&page=1'); - + it('should make an API request when using tabs', (done) => { setTimeout(() => { - component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); - expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2'); - done(); - }, 0); - }); + spyOn(component, 'updateContent'); + component.$el.querySelector('.js-environments-tab-stopped').click(); - it('should update url when page and scope are already present and page is first param', (done) => { - spyOn(gl.utils, 'visitUrl'); - window.history.pushState({}, null, '?page=1&scope=all'); - - setTimeout(() => { - component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); - expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all'); + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' }); done(); - }, 0); + }); }); }); }); @@ -180,9 +155,7 @@ describe('Environment', () => { }); it('should render empty state', (done) => { - component = new EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - }); + component = mountComponent(EnvironmentsComponent, mockData); setTimeout(() => { expect( @@ -214,9 +187,7 @@ describe('Environment', () => { beforeEach(() => { Vue.http.interceptors.push(environmentsResponseInterceptor); - component = new EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - }); + component = mountComponent(EnvironmentsComponent, mockData); }); afterEach(() => { @@ -289,4 +260,59 @@ describe('Environment', () => { }); }); }); + + describe('methods', () => { + const environmentsEmptyResponseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(environmentsEmptyResponseInterceptor); + Vue.http.interceptors.push(headersInterceptor); + + component = mountComponent(EnvironmentsComponent, mockData); + spyOn(history, 'pushState').and.stub(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, environmentsEmptyResponseInterceptor, + ); + Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor); + }); + + describe('updateContent', () => { + it('should set given parameters', (done) => { + component.updateContent({ scope: 'stopped', page: '3' }) + .then(() => { + expect(component.page).toEqual('3'); + expect(component.scope).toEqual('stopped'); + expect(component.requestData.scope).toEqual('stopped'); + expect(component.requestData.page).toEqual('3'); + done(); + }) + .catch(done.fail); + }); + }); + + describe('onChangeTab', () => { + it('should set page to 1', () => { + spyOn(component, 'updateContent'); + component.onChangeTab('stopped'); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' }); + }); + }); + + describe('onChangePage', () => { + it('should update page and keep scope', () => { + spyOn(component, 'updateContent'); + component.onChangePage(4); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); + }); + }); + }); }); diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 7e62d356bd2..4ea4d9d7499 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,25 +1,28 @@ import Vue from 'vue'; -import '~/flash'; import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; import { environmentsList } from '../mock_data'; import { headersInterceptor } from '../../helpers/vue_resource_helper'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('Environments Folder View', () => { - preloadFixtures('static/environments/environments_folder_view.html.raw'); - let EnvironmentsFolderViewComponent; + let Component; + let component; + const mockData = { + endpoint: 'environments.json', + folderName: 'review', + canCreateDeployment: true, + canReadEnvironment: true, + cssContainerClass: 'container', + }; beforeEach(() => { - loadFixtures('static/environments/environments_folder_view.html.raw'); - EnvironmentsFolderViewComponent = Vue.extend(environmentsFolderViewComponent); - window.history.pushState({}, null, 'environments/folders/build'); + Component = Vue.extend(environmentsFolderViewComponent); }); afterEach(() => { - window.history.pushState({}, null, '/'); + component.$destroy(); }); - let component; - describe('successfull request', () => { const environmentsResponseInterceptor = (request, next) => { next(request.respondWith(JSON.stringify({ @@ -31,10 +34,10 @@ describe('Environments Folder View', () => { headers: { 'X-nExt-pAge': '2', 'x-page': '1', - 'X-Per-Page': '1', + 'X-Per-Page': '2', 'X-Prev-Page': '', - 'X-TOTAL': '37', - 'X-Total-Pages': '2', + 'X-TOTAL': '20', + 'X-Total-Pages': '10', }, })); }; @@ -43,9 +46,7 @@ describe('Environments Folder View', () => { Vue.http.interceptors.push(environmentsResponseInterceptor); Vue.http.interceptors.push(headersInterceptor); - component = new EnvironmentsFolderViewComponent({ - el: document.querySelector('#environments-folder-list-view'), - }); + component = mountComponent(Component, mockData); }); afterEach(() => { @@ -57,7 +58,7 @@ describe('Environments Folder View', () => { it('should render a table with environments', (done) => { setTimeout(() => { - expect(component.$el.querySelectorAll('table')).toBeDefined(); + expect(component.$el.querySelectorAll('table')).not.toBeNull(); expect( component.$el.querySelector('.environment-name').textContent.trim(), ).toEqual(environmentsList[0].name); @@ -68,11 +69,11 @@ describe('Environments Folder View', () => { it('should render available tab with count', (done) => { setTimeout(() => { expect( - component.$el.querySelector('.js-available-environments-folder-tab').textContent, + component.$el.querySelector('.js-environments-tab-available').textContent, ).toContain('Available'); expect( - component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent, + component.$el.querySelector('.js-environments-tab-available .badge').textContent, ).toContain('0'); done(); }, 0); @@ -81,11 +82,11 @@ describe('Environments Folder View', () => { it('should render stopped tab with count', (done) => { setTimeout(() => { expect( - component.$el.querySelector('.js-stopped-environments-folder-tab').textContent, + component.$el.querySelector('.js-environments-tab-stopped').textContent, ).toContain('Stopped'); expect( - component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent, + component.$el.querySelector('.js-environments-tab-stopped .badge').textContent, ).toContain('1'); done(); }, 0); @@ -94,8 +95,8 @@ describe('Environments Folder View', () => { it('should render parent folder name', (done) => { setTimeout(() => { expect( - component.$el.querySelector('.js-folder-name').textContent, - ).toContain('Environments / build'); + component.$el.querySelector('.js-folder-name').textContent.trim(), + ).toContain('Environments / review'); done(); }, 0); }); @@ -104,52 +105,30 @@ describe('Environments Folder View', () => { it('should render pagination', (done) => { setTimeout(() => { expect( - component.$el.querySelectorAll('.gl-pagination li').length, - ).toEqual(5); + component.$el.querySelectorAll('.gl-pagination'), + ).not.toBeNull(); done(); }, 0); }); - it('should update url when no search params are present', (done) => { - spyOn(gl.utils, 'visitUrl'); + it('should make an API request when changing page', (done) => { + spyOn(component, 'updateContent'); setTimeout(() => { - component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); - expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); - done(); - }, 0); - }); - - it('should update url when page is already present', (done) => { - spyOn(gl.utils, 'visitUrl'); - window.history.pushState({}, null, '?page=1'); + component.$el.querySelector('.gl-pagination .js-last-button a').click(); - setTimeout(() => { - component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); - expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); + expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '10' }); done(); }, 0); }); - it('should update url when page and scope are already present', (done) => { - spyOn(gl.utils, 'visitUrl'); - window.history.pushState({}, null, '?scope=all&page=1'); - + it('should make an API request when using tabs', (done) => { setTimeout(() => { - component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); - expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2'); - done(); - }, 0); - }); - - it('should update url when page and scope are already present and page is first param', (done) => { - spyOn(gl.utils, 'visitUrl'); - window.history.pushState({}, null, '?page=1&scope=all'); + spyOn(component, 'updateContent'); + component.$el.querySelector('.js-environments-tab-stopped').click(); - setTimeout(() => { - component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); - expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all'); + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' }); done(); - }, 0); + }); }); }); }); @@ -172,9 +151,7 @@ describe('Environments Folder View', () => { }); it('should not render a table', (done) => { - component = new EnvironmentsFolderViewComponent({ - el: document.querySelector('#environments-folder-list-view'), - }); + component = mountComponent(Component, mockData); setTimeout(() => { expect( @@ -187,11 +164,11 @@ describe('Environments Folder View', () => { it('should render available tab with count 0', (done) => { setTimeout(() => { expect( - component.$el.querySelector('.js-available-environments-folder-tab').textContent, + component.$el.querySelector('.js-environments-tab-available').textContent, ).toContain('Available'); expect( - component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent, + component.$el.querySelector('.js-environments-tab-available .badge').textContent, ).toContain('0'); done(); }, 0); @@ -200,14 +177,70 @@ describe('Environments Folder View', () => { it('should render stopped tab with count 0', (done) => { setTimeout(() => { expect( - component.$el.querySelector('.js-stopped-environments-folder-tab').textContent, + component.$el.querySelector('.js-environments-tab-stopped').textContent, ).toContain('Stopped'); expect( - component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent, + component.$el.querySelector('.js-environments-tab-stopped .badge').textContent, ).toContain('0'); done(); }, 0); }); }); + + describe('methods', () => { + const environmentsEmptyResponseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(environmentsEmptyResponseInterceptor); + Vue.http.interceptors.push(headersInterceptor); + + component = mountComponent(Component, mockData); + spyOn(history, 'pushState').and.stub(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, environmentsEmptyResponseInterceptor, + ); + Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor); + }); + + describe('updateContent', () => { + it('should set given parameters', (done) => { + component.updateContent({ scope: 'stopped', page: '4' }) + .then(() => { + expect(component.page).toEqual('4'); + expect(component.scope).toEqual('stopped'); + expect(component.requestData.scope).toEqual('stopped'); + expect(component.requestData.page).toEqual('4'); + done(); + }) + .catch(done.fail); + }); + }); + + describe('onChangeTab', () => { + it('should set page to 1', () => { + spyOn(component, 'updateContent'); + component.onChangeTab('stopped'); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' }); + }); + }); + + describe('onChangePage', () => { + it('should update page and keep scope', () => { + spyOn(component, 'updateContent'); + + component.onChangePage(4); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); + }); + }); + }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index f209328dee1..230c15e5de6 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -396,6 +396,25 @@ describe('Filtered Search Manager', () => { }); }); + describe('Clearing search', () => { + beforeEach(() => { + initializeManager(); + }); + + it('Clicking the "x" clear button, clears the input', () => { + const inputValue = 'label:~bug '; + manager.filteredSearchInput.value = inputValue; + manager.filteredSearchInput.dispatchEvent(new Event('input')); + + expect(gl.DropdownUtils.getSearchQuery()).toEqual(inputValue); + + manager.clearSearchButton.click(); + + expect(manager.filteredSearchInput.value).toEqual(''); + expect(gl.DropdownUtils.getSearchQuery()).toEqual(''); + }); + }); + describe('toggleInputContainerFocus', () => { beforeEach(() => { initializeManager(); diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb index 8e74c4f859c..d26ea3febe8 100644 --- a/spec/javascripts/fixtures/clusters.rb +++ b/spec/javascripts/fixtures/clusters.rb @@ -31,4 +31,19 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle expect(response).to be_success store_frontend_fixture(response, example.description) end + + context 'rendering non-empty state' do + before do + cluster + end + + it 'clusters/index_cluster.html.raw' do |example| + get :index, + namespace_id: namespace, + project_id: project + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end end diff --git a/spec/javascripts/fixtures/environments/element.html.haml b/spec/javascripts/fixtures/environments/element.html.haml deleted file mode 100644 index 8d7aeb23356..00000000000 --- a/spec/javascripts/fixtures/environments/element.html.haml +++ /dev/null @@ -1 +0,0 @@ -.test-dom-element diff --git a/spec/javascripts/fixtures/environments/environments.html.haml b/spec/javascripts/fixtures/environments/environments.html.haml deleted file mode 100644 index e6000fbb553..00000000000 --- a/spec/javascripts/fixtures/environments/environments.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -%div - #environments-list-view{ data: { environments_data: "foo/environments", - "can-create-deployment" => "true", - "can-read-environment" => "true", - "can-create-environment" => "true", - "project-environments-path" => "https://gitlab.com/foo/environments", - "project-stopped-environments-path" => "https://gitlab.com/foo/environments?scope=stopped", - "new-environment-path" => "https://gitlab.com/foo/environments/new", - "help-page-path" => "https://gitlab.com/help_page"}} diff --git a/spec/javascripts/fixtures/environments/environments_folder_view.html.haml b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml deleted file mode 100644 index aceec139730..00000000000 --- a/spec/javascripts/fixtures/environments/environments_folder_view.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%div - #environments-folder-list-view{ data: { "can-create-deployment" => "true", - "can-read-environment" => "true", - "css-class" => "", - "commit-icon-svg" => custom_icon("icon_commit"), - "terminal-icon-svg" => custom_icon("icon_terminal"), - "play-icon-svg" => custom_icon("icon_play") } } diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml index 97b0c25c923..85ee61f0b54 100644 --- a/spec/javascripts/fixtures/pipelines.html.haml +++ b/spec/javascripts/fixtures/pipelines.html.haml @@ -1,16 +1,10 @@ %div #pipelines-list-vue{ data: { endpoint: 'foo', - "css-class" => 'foo', "help-page-path" => 'foo', + "help-auto-devops-path" => 'foo', "empty-state-svg-path" => 'foo', "error-state-svg-path" => 'foo', "new-pipeline-path" => 'foo', "can-create-pipeline" => 'true', - "all-path" => 'foo', - "pending-path" => 'foo', - "running-path" => 'foo', - "finished-path" => 'foo', - "branches-path" => 'foo', - "tags-path" => 'foo', "has-ci" => 'foo', "ci-lint-path" => 'foo' } } diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js index b669aabcee4..97e3ab682c5 100644 --- a/spec/javascripts/flash_spec.js +++ b/spec/javascripts/flash_spec.js @@ -278,7 +278,7 @@ describe('Flash', () => { removeFlashClickListener(flashEl, false); - flashEl.parentNode.click(); + flashEl.click(); setTimeout(() => { expect(document.querySelector('.flash')).toBeNull(); diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js index ad0c7264616..6f357306ec7 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js +++ b/spec/javascripts/gfm_auto_complete_spec.js @@ -67,6 +67,28 @@ describe('GfmAutoComplete', function () { }); }); + describe('DefaultOptions.beforeInsert', () => { + const beforeInsert = (context, value) => ( + gfmAutoCompleteCallbacks.beforeInsert.call(context, value) + ); + + const atwhoInstance = { setting: { skipSpecialCharacterTest: false } }; + + it('should not quote if value only contains alphanumeric charecters', () => { + expect(beforeInsert(atwhoInstance, '@user1')).toBe('@user1'); + expect(beforeInsert(atwhoInstance, '~label1')).toBe('~label1'); + }); + + it('should quote if value contains any non-alphanumeric characters', () => { + expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"'); + expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"'); + }); + + it('should quote integer labels', () => { + expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"'); + }); + }); + describe('DefaultOptions.matcher', function () { const defaultMatcher = (context, flag, subtext) => ( gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext) diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/javascripts/image_diff/helpers/utils_helper_spec.js index 56d77a05c4c..31949c39d9c 100644 --- a/spec/javascripts/image_diff/helpers/utils_helper_spec.js +++ b/spec/javascripts/image_diff/helpers/utils_helper_spec.js @@ -157,27 +157,19 @@ describe('utilsHelper', () => { beforeEach(() => { window.gl = window.gl || (window.gl = {}); glCache = window.gl; - window.gl.ImageFile = () => {}; fileEl = document.createElement('div'); fileEl.innerHTML = ` <div class="diff-file"></div> `; - spyOn(ImageDiff.prototype, 'init').and.callFake(() => {}); spyOn(ReplacedImageDiff.prototype, 'init').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'init').and.callFake(() => {}); }); afterEach(() => { window.gl = glCache; }); - it('should initialize gl.ImageFile', () => { - spyOn(window.gl, 'ImageFile'); - - utilsHelper.initImageDiff(fileEl, false, false); - expect(gl.ImageFile).toHaveBeenCalled(); - }); - it('should initialize ImageDiff if js-single-image', () => { const diffFileEl = fileEl.querySelector('.diff-file'); diffFileEl.innerHTML = ` diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js index ceee08d47c5..5a9112716f4 100644 --- a/spec/javascripts/issuable_spec.js +++ b/spec/javascripts/issuable_spec.js @@ -26,7 +26,7 @@ describe('Issuable', () => { document.body.appendChild(element); const input = document.createElement('input'); - input.setAttribute('id', 'issue_email'); + input.setAttribute('id', 'issuable_email'); document.body.appendChild(input); Issuable = new IssuableIndex('issue_'); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 2ea290108a4..b47a8bf705f 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -35,11 +35,12 @@ describe('Issuable output', () => { canUpdate: true, canDestroy: true, endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes', + updateEndpoint: gl.TEST_HOST, issuableRef: '#1', initialTitleHtml: '', initialTitleText: '', - initialDescriptionHtml: '', - initialDescriptionText: '', + initialDescriptionHtml: 'test', + initialDescriptionText: 'test', markdownPreviewPath: '/', markdownDocsPath: '/', projectNamespace: '/', @@ -223,23 +224,46 @@ describe('Issuable output', () => { }); }); - it('closes form on error', (done) => { - spyOn(window, 'Flash').and.callThrough(); - spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => { - reject(); - })); + describe('error when updating', () => { + beforeEach(() => { + spyOn(window, 'Flash').and.callThrough(); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => { + reject(); + })); + }); - vm.updateIssuable(); + it('closes form on error', (done) => { + vm.updateIssuable(); - setTimeout(() => { - expect( - eventHub.$emit, - ).toHaveBeenCalledWith('close.form'); - expect( - window.Flash, - ).toHaveBeenCalledWith('Error updating issue'); + setTimeout(() => { + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + expect( + window.Flash, + ).toHaveBeenCalledWith('Error updating issue'); - done(); + done(); + }); + }); + + it('returns the correct error message for issuableType', (done) => { + vm.issuableType = 'merge request'; + + Vue.nextTick(() => { + vm.updateIssuable(); + + setTimeout(() => { + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + expect( + window.Flash, + ).toHaveBeenCalledWith('Error updating merge request'); + + done(); + }); + }); }); }); }); diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 360691a3546..163e5cdd062 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -1,11 +1,22 @@ import Vue from 'vue'; import descriptionComponent from '~/issue_show/components/description.vue'; +import * as taskList from '~/task_list'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('Description component', () => { let vm; + let DescriptionComponent; + const props = { + canUpdate: true, + descriptionHtml: 'test', + descriptionText: 'test', + updatedAt: new Date().toString(), + taskStatus: '', + updateUrl: gl.TEST_HOST, + }; beforeEach(() => { - const Component = Vue.extend(descriptionComponent); + DescriptionComponent = Vue.extend(descriptionComponent); if (!document.querySelector('.issuable-meta')) { const metaData = document.createElement('div'); @@ -15,15 +26,11 @@ describe('Description component', () => { document.body.appendChild(metaData); } - vm = new Component({ - propsData: { - canUpdate: true, - descriptionHtml: 'test', - descriptionText: 'test', - updatedAt: new Date().toString(), - taskStatus: '', - }, - }).$mount(); + vm = mountComponent(DescriptionComponent, props); + }); + + afterEach(() => { + vm.$destroy(); }); it('animates description changes', (done) => { @@ -44,34 +51,46 @@ describe('Description component', () => { }); }); - // TODO: gl.TaskList no longer exists. rewrite these tests once we have a way to rewire ES modules - - // 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('TaskList', () => { + beforeEach(() => { + vm = mountComponent(DescriptionComponent, Object.assign({}, props, { + issuableType: 'issuableType', + })); + spyOn(taskList, 'default'); + }); + + it('re-inits the TaskList when description changed', (done) => { + vm.descriptionHtml = 'changed'; + + setTimeout(() => { + expect(taskList.default).toHaveBeenCalled(); + done(); + }); + }); + + it('does not re-init the TaskList when canUpdate is false', (done) => { + vm.canUpdate = false; + vm.descriptionHtml = 'changed'; + + setTimeout(() => { + expect(taskList.default).not.toHaveBeenCalled(); + done(); + }); + }); + + it('calls with issuableType dataType', (done) => { + vm.descriptionHtml = 'changed'; + + setTimeout(() => { + expect(taskList.default).toHaveBeenCalledWith({ + dataType: 'issuableType', + fieldName: 'description', + selector: '.detail-page-description', + }); + done(); + }); + }); + }); describe('taskStatus', () => { it('adds full taskStatus', (done) => { @@ -126,4 +145,8 @@ describe('Description component', () => { }); }); }); + + it('sets data-update-url', () => { + expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(gl.TEST_HOST); + }); }); diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js index f6625b748b6..d779ab7bb31 100644 --- a/spec/javascripts/issue_show/components/edit_actions_spec.js +++ b/spec/javascripts/issue_show/components/edit_actions_spec.js @@ -61,6 +61,15 @@ describe('Edit Actions components', () => { }); }); + it('should not show delete button if showDeleteButton is false', (done) => { + vm.showDeleteButton = false; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-danger')).toBeNull(); + done(); + }); + }); + describe('updateIssuable', () => { it('sends update.issauble event when clicking save button', () => { vm.$el.querySelector('.btn-save').click(); diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js index 6e89528a3ea..000b53af016 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -34,7 +34,6 @@ describe('Inline edit form component', () => { }); it('renders template selector when templates exists', (done) => { - spyOn(gl, 'IssuableTemplateSelectors'); vm.issuableTemplates = ['test']; Vue.nextTick(() => { diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js index c1edc785d0f..5370f4e1fea 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -80,19 +80,19 @@ describe('Title component', () => { }); it('should not show by default', () => { - expect(vm.$el.querySelector('.note-action-button')).toBeNull(); + expect(vm.$el.querySelector('.btn-edit')).toBeNull(); }); it('should not show if canUpdate is false', () => { vm.showInlineEditButton = true; vm.canUpdate = false; - expect(vm.$el.querySelector('.note-action-button')).toBeNull(); + expect(vm.$el.querySelector('.btn-edit')).toBeNull(); }); it('should show if showInlineEditButton and canUpdate', () => { vm.showInlineEditButton = true; vm.canUpdate = true; - expect(vm.$el.querySelector('.note-action-button')).toBeDefined(); + expect(vm.$el.querySelector('.btn-edit')).toBeDefined(); }); it('should trigger open.form event when clicked', () => { @@ -100,7 +100,7 @@ describe('Title component', () => { vm.canUpdate = true; Vue.nextTick(() => { - vm.$el.querySelector('.note-action-button').click(); + vm.$el.querySelector('.btn-edit').click(); expect(eventHub.$emit).toHaveBeenCalledWith('open.form'); }); }); diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index 5e67911d338..20c4caa865d 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -28,7 +28,7 @@ describe('Job', () => { }); it('copies build options', function () { - expect(this.job.pageUrl).toBe(JOB_URL); + expect(this.job.pagePath).toBe(JOB_URL); expect(this.job.buildStatus).toBe('success'); expect(this.job.buildStage).toBe('test'); expect(this.job.state).toBe(''); diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js index 1d7fa7e12fc..3069a0cd60e 100644 --- a/spec/javascripts/jobs/job_details_mediator_spec.js +++ b/spec/javascripts/jobs/job_details_mediator_spec.js @@ -1,39 +1,35 @@ -import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import JobMediator from '~/jobs/job_details_mediator'; import job from './mock_data'; describe('JobMediator', () => { let mediator; + let mock; beforeEach(() => { - mediator = new JobMediator({ endpoint: 'foo' }); + mediator = new JobMediator({ endpoint: 'jobs/40291672.json' }); + mock = new MockAdapter(axios); }); it('should set defaults', () => { expect(mediator.store).toBeDefined(); expect(mediator.service).toBeDefined(); - expect(mediator.options).toEqual({ endpoint: 'foo' }); + expect(mediator.options).toEqual({ endpoint: 'jobs/40291672.json' }); expect(mediator.state.isLoading).toEqual(false); }); describe('request and store data', () => { - const interceptor = (request, next) => { - next(request.respondWith(JSON.stringify(job), { - status: 200, - })); - }; - beforeEach(() => { - Vue.http.interceptors.push(interceptor); + mock.onGet().reply(200, job, {}); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor); + mock.restore(); }); it('should store received data', (done) => { mediator.fetchJob(); - setTimeout(() => { expect(mediator.store.state.job).toEqual(job); done(); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index a5298be5669..0a9d815f469 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -142,44 +142,33 @@ describe('common_utils', () => { }); }); - describe('setParamInURL', () => { + describe('historyPushState', () => { afterEach(() => { - window.history.pushState({}, null, ''); + window.history.replaceState({}, null, null); }); - it('should return the parameter', () => { - window.history.replaceState({}, null, ''); - - expect(commonUtils.setParamInURL('page', 156)).toBe('?page=156'); - expect(commonUtils.setParamInURL('page', '156')).toBe('?page=156'); - }); + it('should call pushState with the correct path', () => { + spyOn(window.history, 'pushState'); - it('should update the existing parameter when its a number', () => { - window.history.pushState({}, null, '?page=15'); + commonUtils.historyPushState('newpath?page=2'); - expect(commonUtils.setParamInURL('page', 16)).toBe('?page=16'); - expect(commonUtils.setParamInURL('page', '16')).toBe('?page=16'); - expect(commonUtils.setParamInURL('page', true)).toBe('?page=true'); + expect(window.history.pushState).toHaveBeenCalled(); + expect(window.history.pushState.calls.allArgs()[0][2]).toContain('newpath?page=2'); }); + }); - it('should update the existing parameter when its a string', () => { - window.history.pushState({}, null, '?scope=all'); - - expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished'); - }); - - it('should update the existing parameter when more than one parameter exists', () => { - window.history.pushState({}, null, '?scope=all&page=15'); - - expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15'); + describe('parseQueryStringIntoObject', () => { + it('should return object with query parameters', () => { + expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' }); + expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' }); + expect(commonUtils.parseQueryStringIntoObject()).toEqual({}); }); + }); - it('should add a new parameter to the end of the existing ones', () => { - window.history.pushState({}, null, '?scope=all'); - - expect(commonUtils.setParamInURL('page', 16)).toBe('?scope=all&page=16'); - expect(commonUtils.setParamInURL('page', '16')).toBe('?scope=all&page=16'); - expect(commonUtils.setParamInURL('page', true)).toBe('?scope=all&page=true'); + describe('buildUrlWithCurrentLocation', () => { + it('should build an url with current location and given parameters', () => { + expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); + expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`); }); }); diff --git a/spec/javascripts/lib/utils/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js index e58ac4300ba..a9f3abcf2a4 100644 --- a/spec/javascripts/lib/utils/datefix_spec.js +++ b/spec/javascripts/lib/utils/datefix_spec.js @@ -21,7 +21,7 @@ describe('datefix', () => { describe('pikadayToString', () => { it('should format a UTC date into yyyy-mm-dd format', () => { - expect(pikadayToString(new Date('2020-01-29'))).toEqual('2020-01-29'); + expect(pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29'); }); }); }); diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js index 83c92deccdc..fcf27f6805f 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, bytesToMiB } from '~/lib/utils/number_utils'; +import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils'; describe('Number Utils', () => { describe('formatRelevantDigits', () => { @@ -52,4 +52,29 @@ describe('Number Utils', () => { expect(bytesToMiB(1000000)).toEqual(0.95367431640625); }); }); + + describe('bytesToGiB', () => { + it('calculates GiB for the given bytes', () => { + expect(bytesToGiB(1073741824)).toEqual(1); + expect(bytesToGiB(10737418240)).toEqual(10); + }); + }); + + describe('numberToHumanSize', () => { + it('should return bytes', () => { + expect(numberToHumanSize(654)).toEqual('654 bytes'); + }); + + it('should return KiB', () => { + expect(numberToHumanSize(1079)).toEqual('1.05 KiB'); + }); + + it('should return MiB', () => { + expect(numberToHumanSize(10485764)).toEqual('10.00 MiB'); + }); + + it('should return GiB', () => { + expect(numberToHumanSize(10737418240)).toEqual('10.00 GiB'); + }); + }); }); diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index 2aa7011ca51..9b8f68f1676 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -155,7 +155,7 @@ describe('Poll', () => { successCallback: () => { Polling.stop(); setTimeout(() => { - Polling.restart(); + Polling.restart({ data: { page: 4 } }); }, 0); }, errorCallback: callbacks.error, @@ -170,10 +170,10 @@ describe('Poll', () => { Polling.stop(); expect(service.fetch.calls.count()).toEqual(2); - expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); + expect(service.fetch).toHaveBeenCalledWith({ page: 4 }); expect(Polling.stop).toHaveBeenCalled(); expect(Polling.restart).toHaveBeenCalled(); - + expect(Polling.options.data).toEqual({ page: 4 }); done(); }); }); diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js new file mode 100644 index 00000000000..a95a7e2a5be --- /dev/null +++ b/spec/javascripts/lib/utils/text_markdown_spec.js @@ -0,0 +1,62 @@ +import textUtils from '~/lib/utils/text_markdown'; + +describe('init markdown', () => { + let textArea; + + beforeAll(() => { + textArea = document.createElement('textarea'); + document.querySelector('body').appendChild(textArea); + textArea.focus(); + }); + + afterAll(() => { + textArea.parentNode.removeChild(textArea); + }); + + describe('without selection', () => { + it('inserts the tag on an empty line', () => { + const initialValue = ''; + + textArea.value = initialValue; + textArea.selectionStart = 0; + textArea.selectionEnd = 0; + + textUtils.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + + it('inserts the tag on a new line if the current one is not empty', () => { + const initialValue = 'some text'; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + textUtils.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}\n* `); + }); + + it('inserts the tag on the same line if the current line only contains spaces', () => { + const initialValue = ' '; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + textUtils.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + + it('inserts the tag on the same line if the current line only contains tabs', () => { + const initialValue = '\t\t\t'; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + textUtils.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 829b3ef5735..1f46c225071 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -1,109 +1,65 @@ -import { highCountTrim } from '~/lib/utils/text_utility'; +import * as textUtils from '~/lib/utils/text_utility'; describe('text_utility', () => { - describe('gl.text.getTextWidth', () => { - it('returns zero width when no text is passed', () => { - expect(gl.text.getTextWidth('')).toBe(0); + describe('addDelimiter', () => { + it('should add a delimiter to the given string', () => { + expect(textUtils.addDelimiter('1234')).toEqual('1,234'); + expect(textUtils.addDelimiter('222222')).toEqual('222,222'); }); - it('returns zero width when no text is passed and font is passed', () => { - expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); + it('should not add a delimiter if string contains no numbers', () => { + expect(textUtils.addDelimiter('aaaa')).toEqual('aaaa'); }); + }); - it('returns width when text is passed', () => { - expect(gl.text.getTextWidth('foo') > 0).toBe(true); + describe('highCountTrim', () => { + it('returns 99+ for count >= 100', () => { + expect(textUtils.highCountTrim(105)).toBe('99+'); + expect(textUtils.highCountTrim(100)).toBe('99+'); }); - it('returns bigger width when font is larger', () => { - const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); - const regular = gl.text.getTextWidth('foo', '10px sans-serif'); - expect(largeFont > regular).toBe(true); + it('returns exact number for count < 100', () => { + expect(textUtils.highCountTrim(45)).toBe(45); }); }); - describe('gl.text.pluralize', () => { - it('returns pluralized', () => { - expect(gl.text.pluralize('test', 2)).toBe('tests'); - }); - - it('returns pluralized when count is 0', () => { - expect(gl.text.pluralize('test', 0)).toBe('tests'); + describe('capitalizeFirstCharacter', () => { + it('returns string with first letter capitalized', () => { + expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab'); + expect(textUtils.highCountTrim(105)).toBe('99+'); + expect(textUtils.highCountTrim(100)).toBe('99+'); }); + }); - it('does not return pluralized', () => { - expect(gl.text.pluralize('test', 1)).toBe('test'); + describe('humanize', () => { + it('should remove underscores and uppercase the first letter', () => { + expect(textUtils.humanize('foo_bar')).toEqual('Foo bar'); }); }); - describe('highCountTrim', () => { - it('returns 99+ for count >= 100', () => { - expect(highCountTrim(105)).toBe('99+'); - expect(highCountTrim(100)).toBe('99+'); + describe('pluralize', () => { + it('should pluralize given string', () => { + expect(textUtils.pluralize('test', 2)).toBe('tests'); }); - it('returns exact number for count < 100', () => { - expect(highCountTrim(45)).toBe(45); + it('should pluralize when count is 0', () => { + expect(textUtils.pluralize('test', 0)).toBe('tests'); }); - }); - - describe('gl.text.insertText', () => { - let textArea; - beforeAll(() => { - textArea = document.createElement('textarea'); - document.querySelector('body').appendChild(textArea); - textArea.focus(); + it('should not pluralize when count is 1', () => { + expect(textUtils.pluralize('test', 1)).toBe('test'); }); + }); - afterAll(() => { - textArea.parentNode.removeChild(textArea); + describe('dasherize', () => { + it('should replace underscores with dashes', () => { + expect(textUtils.dasherize('foo_bar_foo')).toEqual('foo-bar-foo'); }); + }); - describe('without selection', () => { - it('inserts the tag on an empty line', () => { - const initialValue = ''; - - textArea.value = initialValue; - textArea.selectionStart = 0; - textArea.selectionEnd = 0; - - gl.text.insertText(textArea, textArea.value, '*', null, '', false); - - expect(textArea.value).toEqual(`${initialValue}* `); - }); - - it('inserts the tag on a new line if the current one is not empty', () => { - const initialValue = 'some text'; - - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); - - gl.text.insertText(textArea, textArea.value, '*', null, '', false); - - expect(textArea.value).toEqual(`${initialValue}\n* `); - }); - - it('inserts the tag on the same line if the current line only contains spaces', () => { - const initialValue = ' '; - - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); - - gl.text.insertText(textArea, textArea.value, '*', null, '', false); - - expect(textArea.value).toEqual(`${initialValue}* `); - }); - - it('inserts the tag on the same line if the current line only contains tabs', () => { - const initialValue = '\t\t\t'; - - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); - - gl.text.insertText(textArea, textArea.value, '*', null, '', false); - - expect(textArea.value).toEqual(`${initialValue}* `); - }); + describe('slugify', () => { + it('should remove accents and convert to lower case', () => { + expect(textUtils.slugify('João')).toEqual('joão'); }); }); }); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 3ab901da6b6..70ae63ba036 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -63,7 +63,7 @@ import IssuablesHelper from '~/helpers/issuables_helper'; describe('merge request of another user', () => { beforeEach(() => { loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - this.el = document.querySelector('.merge-request .issuable-actions'); + this.el = document.querySelector('.js-issuable-actions'); const merge = new MergeRequest(); merge.hideCloseButton(); }); @@ -83,7 +83,7 @@ import IssuablesHelper from '~/helpers/issuables_helper'; describe('merge request of current_user', () => { beforeEach(() => { loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); - this.el = document.querySelector('.merge-request .issuable-actions'); + this.el = document.querySelector('.js-issuable-actions'); const merge = new MergeRequest(); merge.hideCloseButton(); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 752fdfb4614..9885b8a790f 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -1,6 +1,8 @@ import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; import Dashboard from '~/monitoring/components/dashboard.vue'; -import { MonitorMockInterceptor } from './mock_data'; +import axios from '~/lib/utils/axios_utils'; +import { metricsGroupsAPIResponse, mockApiEndpoint } from './mock_data'; describe('Dashboard', () => { const fixtureName = 'environments/metrics/metrics.html.raw'; @@ -26,13 +28,17 @@ describe('Dashboard', () => { }); describe('requests information to the server', () => { + let mock; beforeEach(() => { document.querySelector('#prometheus-graphs').setAttribute('data-has-metrics', 'true'); - Vue.http.interceptors.push(MonitorMockInterceptor); + mock = new MockAdapter(axios); + mock.onGet(mockApiEndpoint).reply(200, { + metricsGroupsAPIResponse, + }); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, MonitorMockInterceptor); + mock.reset(); }); it('shows up a loading state', (done) => { diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js index 8ece913ada8..c83bd19345f 100644 --- a/spec/javascripts/monitoring/graph_path_spec.js +++ b/spec/javascripts/monitoring/graph_path_spec.js @@ -32,4 +32,21 @@ describe('Monitoring Paths', () => { expect(metricLine.getAttribute('stroke')).toBe('#1f78d1'); expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath); }); + + describe('Computed properties', () => { + it('strokeDashArray', () => { + const component = createComponent({ + generatedLinePath: firstTimeSeries.linePath, + generatedAreaPath: firstTimeSeries.areaPath, + lineColor: firstTimeSeries.lineColor, + areaColor: firstTimeSeries.areaColor, + }); + + component.lineStyle = 'dashed'; + expect(component.strokeDashArray).toBe('3, 1'); + + component.lineStyle = 'dotted'; + expect(component.strokeDashArray).toBe('1, 1'); + }); + }); }); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 7ceab657464..6b34855b8b2 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -2425,13 +2425,6 @@ const metricsGroupsAPIResponse = { export default metricsGroupsAPIResponse; -const responseMockData = { - 'GET': { - '/root/hello-prometheus/environments/30/additional_metrics.json': metricsGroupsAPIResponse, - 'http://test.host/frontend-fixtures/environments-project/environments/1/additional_metrics.json': metricsGroupsAPIResponse, // TODO: MAke sure this works in the monitoring_bundle_spec - }, -}; - export const deploymentData = [ { id: 111, @@ -8320,11 +8313,3 @@ export function convertDatesMultipleSeries(multipleSeries) { }); return convertedMultiple; } - -export function MonitorMockInterceptor(request, next) { - const body = responseMockData[request.method.toUpperCase()][request.url]; - - next(request.respondWith(JSON.stringify(body), { - status: 200, - })); -} diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index c57f44dae17..50a5e4ff056 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ -/* global NewBranchForm */ -import '~/new_branch_form'; +import NewBranchForm from '~/new_branch_form'; (function() { describe('Branch', function() { diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js index a26fc8f63cc..04a7f8e32f1 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import Autosize from 'autosize'; import store from '~/notes/stores'; import issueCommentForm from '~/notes/components/issue_comment_form.vue'; -import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data'; +import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data'; import { keyboardDownEvent } from '../../issue_show/helpers'; describe('issue_comment_form component', () => { @@ -23,7 +23,7 @@ describe('issue_comment_form component', () => { describe('user is logged in', () => { beforeEach(() => { store.dispatch('setUserData', userDataMock); - store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); vm = mountComponent(); @@ -55,6 +55,25 @@ describe('issue_comment_form component', () => { expect(vm.toggleIssueState).toHaveBeenCalled(); }); + + it('should disable action button whilst submitting', (done) => { + const saveNotePromise = Promise.resolve(); + vm.note = 'hello world'; + spyOn(vm, 'saveNote').and.returnValue(saveNotePromise); + spyOn(vm, 'stopPolling'); + + const actionButton = vm.$el.querySelector('.js-action-button'); + + vm.handleSave(); + + Vue.nextTick() + .then(() => expect(actionButton.disabled).toBeTruthy()) + .then(saveNotePromise) + .then(Vue.nextTick) + .then(() => expect(actionButton.disabled).toBeFalsy()) + .then(done) + .catch(done.fail); + }); }); describe('textarea', () => { @@ -159,7 +178,7 @@ describe('issue_comment_form component', () => { describe('issue is confidential', () => { it('shows information warning', (done) => { - store.dispatch('setIssueData', Object.assign(issueDataMock, { confidential: true })); + store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true })); Vue.nextTick(() => { expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined(); done(); @@ -171,7 +190,7 @@ describe('issue_comment_form component', () => { describe('user is not logged in', () => { beforeEach(() => { store.dispatch('setUserData', null); - store.dispatch('setIssueData', loggedOutIssueData); + store.dispatch('setNoteableData', loggedOutnoteableData); store.dispatch('setNotesData', notesDataMock); vm = mountComponent(); diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/issue_discussion_spec.js index 05c6b57f93e..b6ae55d44f5 100644 --- a/spec/javascripts/notes/components/issue_discussion_spec.js +++ b/spec/javascripts/notes/components/issue_discussion_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/notes/stores'; import issueDiscussion from '~/notes/components/issue_discussion.vue'; -import { issueDataMock, discussionMock, notesDataMock } from '../mock_data'; +import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; describe('issue_discussion component', () => { let vm; @@ -9,7 +9,7 @@ describe('issue_discussion component', () => { beforeEach(() => { const Component = Vue.extend(issueDiscussion); - store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); vm = new Component({ diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js index 22e91c4c40f..8e43037f356 100644 --- a/spec/javascripts/notes/components/issue_note_app_spec.js +++ b/spec/javascripts/notes/components/issue_note_app_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import issueNotesApp from '~/notes/components/issue_notes_app.vue'; -import service from '~/notes/services/issue_notes_service'; +import service from '~/notes/services/notes_service'; import * as mockData from '../mock_data'; describe('issue_note_app', () => { @@ -24,7 +24,7 @@ describe('issue_note_app', () => { mountComponent = (data) => { const props = data || { - issueData: mockData.issueDataMock, + noteableData: mockData.noteableDataMock, notesData: mockData.notesDataMock, userData: mockData.userDataMock, }; @@ -60,7 +60,7 @@ describe('issue_note_app', () => { }); it('should set issue data', () => { - expect(vm.$store.state.issueData).toEqual(mockData.issueDataMock); + expect(vm.$store.state.noteableData).toEqual(mockData.noteableDataMock); }); it('should set user data', () => { diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/issue_note_body_spec.js index 81f07ed47cc..37aad50737b 100644 --- a/spec/javascripts/notes/components/issue_note_body_spec.js +++ b/spec/javascripts/notes/components/issue_note_body_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import store from '~/notes/stores'; import noteBody from '~/notes/components/issue_note_body.vue'; -import { issueDataMock, notesDataMock, note } from '../mock_data'; +import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note_body component', () => { let vm; @@ -10,7 +10,7 @@ describe('issue_note_body component', () => { beforeEach(() => { const Component = Vue.extend(noteBody); - store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); vm = new Component({ diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/issue_note_form_spec.js index a90dbcb72b5..d42ef239711 100644 --- a/spec/javascripts/notes/components/issue_note_form_spec.js +++ b/spec/javascripts/notes/components/issue_note_form_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/notes/stores'; import issueNoteForm from '~/notes/components/issue_note_form.vue'; -import { issueDataMock, notesDataMock } from '../mock_data'; +import { noteableDataMock, notesDataMock } from '../mock_data'; import { keyboardDownEvent } from '../../issue_show/helpers'; describe('issue_note_form component', () => { @@ -11,7 +11,7 @@ describe('issue_note_form component', () => { beforeEach(() => { const Component = Vue.extend(issueNoteForm); - store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); props = { diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/issue_note_spec.js index 7ef85d5b4f0..73fd188dbe5 100644 --- a/spec/javascripts/notes/components/issue_note_spec.js +++ b/spec/javascripts/notes/components/issue_note_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import store from '~/notes/stores'; import issueNote from '~/notes/components/issue_note.vue'; -import { issueDataMock, notesDataMock, note } from '../mock_data'; +import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note', () => { let vm; @@ -10,7 +10,7 @@ describe('issue_note', () => { beforeEach(() => { const Component = Vue.extend(issueNote); - store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); vm = new Component({ diff --git a/spec/javascripts/notes/components/issue_note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index 7bcc061f167..ab81aabb992 100644 --- a/spec/javascripts/notes/components/issue_note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import issueActions from '~/notes/components/issue_note_actions.vue'; +import noteActions from '~/notes/components/note_actions.vue'; import { userDataMock } from '../mock_data'; describe('issse_note_actions component', () => { @@ -8,7 +8,7 @@ describe('issse_note_actions component', () => { let Component; beforeEach(() => { - Component = Vue.extend(issueActions); + Component = Vue.extend(noteActions); }); afterEach(() => { diff --git a/spec/javascripts/notes/components/issue_note_attachment_spec.js b/spec/javascripts/notes/components/note_attachment_spec.js index 8f33b874ad6..b14a518b622 100644 --- a/spec/javascripts/notes/components/issue_note_attachment_spec.js +++ b/spec/javascripts/notes/components/note_attachment_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import issueNoteAttachment from '~/notes/components/issue_note_attachment.vue'; +import noteAttachment from '~/notes/components/note_attachment.vue'; describe('issue note attachment', () => { it('should render properly', () => { @@ -11,7 +11,7 @@ describe('issue note attachment', () => { }, }; - const Component = Vue.extend(issueNoteAttachment); + const Component = Vue.extend(noteAttachment); const vm = new Component({ propsData: props, }).$mount(); diff --git a/spec/javascripts/notes/components/issue_note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js index 3b6c34f1494..15995ec5a05 100644 --- a/spec/javascripts/notes/components/issue_note_awards_list_spec.js +++ b/spec/javascripts/notes/components/note_awards_list_spec.js @@ -1,16 +1,16 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import awardsNote from '~/notes/components/issue_note_awards_list.vue'; -import { issueDataMock, notesDataMock } from '../mock_data'; +import awardsNote from '~/notes/components/note_awards_list.vue'; +import { noteableDataMock, notesDataMock } from '../mock_data'; -describe('issue_note_awards_list component', () => { +describe('note_awards_list component', () => { let vm; let awardsMock; beforeEach(() => { const Component = Vue.extend(awardsNote); - store.dispatch('setIssueData', issueDataMock); + store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); awardsMock = [ { diff --git a/spec/javascripts/notes/components/issue_note_edited_text_spec.js b/spec/javascripts/notes/components/note_edited_text_spec.js index 6603241eb64..e0b991c32ec 100644 --- a/spec/javascripts/notes/components/issue_note_edited_text_spec.js +++ b/spec/javascripts/notes/components/note_edited_text_spec.js @@ -1,12 +1,12 @@ import Vue from 'vue'; -import issueNoteEditedText from '~/notes/components/issue_note_edited_text.vue'; +import noteEditedText from '~/notes/components/note_edited_text.vue'; -describe('issue_note_edited_text', () => { +describe('note_edited_text', () => { let vm; let props; beforeEach(() => { - const Component = Vue.extend(issueNoteEditedText); + const Component = Vue.extend(noteEditedText); props = { actionText: 'Edited', className: 'foo-bar', diff --git a/spec/javascripts/notes/components/issue_note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js index 83ea18508ae..16a76b11321 100644 --- a/spec/javascripts/notes/components/issue_note_header_spec.js +++ b/spec/javascripts/notes/components/note_header_spec.js @@ -1,13 +1,13 @@ import Vue from 'vue'; -import issueNoteHeader from '~/notes/components/issue_note_header.vue'; +import noteHeader from '~/notes/components/note_header.vue'; import store from '~/notes/stores'; -describe('issue_note_header component', () => { +describe('note_header component', () => { let vm; let Component; beforeEach(() => { - Component = Vue.extend(issueNoteHeader); + Component = Vue.extend(noteHeader); }); afterEach(() => { diff --git a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js b/spec/javascripts/notes/components/note_signed_out_widget_spec.js index f20d9ce9268..6cba8053888 100644 --- a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js +++ b/spec/javascripts/notes/components/note_signed_out_widget_spec.js @@ -1,13 +1,13 @@ import Vue from 'vue'; -import issueNoteSignedOut from '~/notes/components/issue_note_signed_out_widget.vue'; +import noteSignedOut from '~/notes/components/note_signed_out_widget.vue'; import store from '~/notes/stores'; import { notesDataMock } from '../mock_data'; -describe('issue_note_signed_out_widget component', () => { +describe('note_signed_out_widget component', () => { let vm; beforeEach(() => { - const Component = Vue.extend(issueNoteSignedOut); + const Component = Vue.extend(noteSignedOut); store.dispatch('setNotesData', notesDataMock); vm = new Component({ diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 89ba3a002b7..42497de3c55 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -18,7 +18,7 @@ export const userDataMock = { username: 'root', }; -export const issueDataMock = { +export const noteableDataMock = { assignees: [], author_id: 1, branch_name: null, @@ -271,7 +271,7 @@ export const discussionMock = { individual_note: false, }; -export const loggedOutIssueData = { +export const loggedOutnoteableData = { "id": 98, "iid": 26, "author_id": 1, diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 3d1ca870ca4..e092320f9a3 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,6 +1,6 @@ import * as actions from '~/notes/stores/actions'; import testAction from '../../helpers/vuex_action_helper'; -import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; +import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; describe('Actions Notes Store', () => { describe('setNotesData', () => { @@ -11,10 +11,10 @@ describe('Actions Notes Store', () => { }); }); - describe('setIssueData', () => { + describe('setNoteableData', () => { it('should set received issue data', (done) => { - testAction(actions.setIssueData, null, { issueData: {} }, [ - { type: 'SET_ISSUE_DATA', payload: issueDataMock }, + testAction(actions.setNoteableData, null, { noteableData: {} }, [ + { type: 'SET_NOTEABLE_DATA', payload: noteableDataMock }, ], done); }); }); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index 48ee1bf9a52..c5a84b71788 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -1,5 +1,5 @@ import * as getters from '~/notes/stores/getters'; -import { notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; +import { notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; describe('Getters Notes Store', () => { let state; @@ -11,7 +11,7 @@ describe('Getters Notes Store', () => { notesData: notesDataMock, userData: userDataMock, - issueData: issueDataMock, + noteableData: noteableDataMock, }; }); describe('notes', () => { @@ -32,9 +32,9 @@ describe('Getters Notes Store', () => { }); }); - describe('getIssueData', () => { - it('should return all data in `issueData`', () => { - expect(getters.getIssueData(state)).toEqual(issueDataMock); + describe('getNoteableData', () => { + it('should return all data in `noteableData`', () => { + expect(getters.getNoteableData(state)).toEqual(noteableDataMock); }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 1e22e03e178..22d99998a7d 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -1,5 +1,5 @@ import mutations from '~/notes/stores/mutations'; -import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; +import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; describe('Mutation Notes Store', () => { describe('ADD_NEW_NOTE', () => { @@ -74,14 +74,14 @@ describe('Mutation Notes Store', () => { }); }); - describe('SET_ISSUE_DATA', () => { + describe('SET_NOTEABLE_DATA', () => { it('should set the issue data', () => { const state = { - issueData: {}, + noteableData: {}, }; - mutations.SET_ISSUE_DATA(state, issueDataMock); - expect(state.issueData).toEqual(issueDataMock); + mutations.SET_NOTEABLE_DATA(state, noteableDataMock); + expect(state.noteableData).toEqual(noteableDataMock); }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 928a4b461cc..677a389b88f 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -5,7 +5,6 @@ import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; import '~/render_gfm'; -import '~/render_math'; import '~/notes'; (function() { diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 342ee6c1242..23c87610d83 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -1,8 +1,10 @@ import Vue from 'vue'; import jobComponent from '~/pipelines/components/graph/job_component.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('pipeline graph job component', () => { let JobComponent; + let component; const mockJob = { id: 4256, @@ -13,6 +15,7 @@ describe('pipeline graph job component', () => { label: 'passed', group: 'success', details_path: '/root/ci-mock/builds/4256', + has_details: true, action: { icon: 'retry', title: 'Retry', @@ -26,13 +29,13 @@ describe('pipeline graph job component', () => { JobComponent = Vue.extend(jobComponent); }); + afterEach(() => { + component.$destroy(); + }); + describe('name with link', () => { it('should render the job name and status with a link', (done) => { - const component = new JobComponent({ - propsData: { - job: mockJob, - }, - }).$mount(); + component = mountComponent(JobComponent, { job: mockJob }); Vue.nextTick(() => { const link = component.$el.querySelector('a'); @@ -56,23 +59,23 @@ describe('pipeline graph job component', () => { describe('name without link', () => { it('it should render status and name', () => { - const component = new JobComponent({ - propsData: { - job: { - id: 4256, - name: 'test', - status: { - icon: 'icon_status_success', - text: 'passed', - label: 'passed', - group: 'success', - details_path: '/root/ci-mock/builds/4256', - }, + component = mountComponent(JobComponent, { + job: { + id: 4256, + name: 'test', + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4256', + has_details: false, }, }, - }).$mount(); + }); expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined(); + expect(component.$el.querySelector('a')).toBeNull(); expect( component.$el.querySelector('.ci-status-text').textContent.trim(), @@ -82,11 +85,7 @@ describe('pipeline graph job component', () => { describe('action icon', () => { it('it should render the action icon', () => { - const component = new JobComponent({ - propsData: { - job: mockJob, - }, - }).$mount(); + component = mountComponent(JobComponent, { job: mockJob }); expect(component.$el.querySelector('a.ci-action-icon-container')).toBeDefined(); expect(component.$el.querySelector('i.ci-action-icon-wrapper')).toBeDefined(); @@ -95,24 +94,20 @@ describe('pipeline graph job component', () => { describe('dropdown', () => { it('should render the dropdown action icon', () => { - const component = new JobComponent({ - propsData: { - job: mockJob, - isDropdown: true, - }, - }).$mount(); + component = mountComponent(JobComponent, { + job: mockJob, + isDropdown: true, + }); expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined(); }); }); it('should render provided class name', () => { - const component = new JobComponent({ - propsData: { - job: mockJob, - cssClassJobName: 'css-class-job-name', - }, - }).$mount(); + component = mountComponent(JobComponent, { + job: mockJob, + cssClassJobName: 'css-class-job-name', + }); expect( component.$el.querySelector('a').classList.contains('css-class-job-name'), diff --git a/spec/javascripts/pipelines/navigation_tabs_spec.js b/spec/javascripts/pipelines/navigation_tabs_spec.js deleted file mode 100644 index 53a88e6322f..00000000000 --- a/spec/javascripts/pipelines/navigation_tabs_spec.js +++ /dev/null @@ -1,127 +0,0 @@ -import Vue from 'vue'; -import navigationTabs from '~/pipelines/components/navigation_tabs.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; - -describe('navigation tabs pipeline component', () => { - let vm; - let Component; - let data; - - beforeEach(() => { - data = { - scope: 'all', - count: { - all: 16, - running: 1, - pending: 10, - finished: 0, - }, - paths: { - allPath: '/gitlab-org/gitlab-ce/pipelines', - pendingPath: '/gitlab-org/gitlab-ce/pipelines?scope=pending', - finishedPath: '/gitlab-org/gitlab-ce/pipelines?scope=finished', - runningPath: '/gitlab-org/gitlab-ce/pipelines?scope=running', - branchesPath: '/gitlab-org/gitlab-ce/pipelines?scope=branches', - tagsPath: '/gitlab-org/gitlab-ce/pipelines?scope=tags', - }, - }; - - Component = Vue.extend(navigationTabs); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render tabs with correct paths', () => { - vm = mountComponent(Component, data); - - // All - const allTab = vm.$el.querySelector('.js-pipelines-tab-all a'); - expect(allTab.textContent.trim()).toContain('All'); - expect(allTab.getAttribute('href')).toEqual(data.paths.allPath); - - // Pending - const pendingTab = vm.$el.querySelector('.js-pipelines-tab-pending a'); - expect(pendingTab.textContent.trim()).toContain('Pending'); - expect(pendingTab.getAttribute('href')).toEqual(data.paths.pendingPath); - - // Running - const runningTab = vm.$el.querySelector('.js-pipelines-tab-running a'); - expect(runningTab.textContent.trim()).toContain('Running'); - expect(runningTab.getAttribute('href')).toEqual(data.paths.runningPath); - - // Finished - const finishedTab = vm.$el.querySelector('.js-pipelines-tab-finished a'); - expect(finishedTab.textContent.trim()).toContain('Finished'); - expect(finishedTab.getAttribute('href')).toEqual(data.paths.finishedPath); - - // Branches - const branchesTab = vm.$el.querySelector('.js-pipelines-tab-branches a'); - expect(branchesTab.textContent.trim()).toContain('Branches'); - - // Tags - const tagsTab = vm.$el.querySelector('.js-pipelines-tab-tags a'); - expect(tagsTab.textContent.trim()).toContain('Tags'); - }); - - describe('scope', () => { - it('should render scope provided as active tab', () => { - vm = mountComponent(Component, data); - expect(vm.$el.querySelector('.js-pipelines-tab-all').className).toContain('active'); - }); - }); - - describe('badges', () => { - it('should render provided number', () => { - vm = mountComponent(Component, data); - // All - expect( - vm.$el.querySelector('.js-totalbuilds-count').textContent.trim(), - ).toContain(data.count.all); - - // Pending - expect( - vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim(), - ).toContain(data.count.pending); - - // Running - expect( - vm.$el.querySelector('.js-pipelines-tab-running .badge').textContent.trim(), - ).toContain(data.count.running); - - // Finished - expect( - vm.$el.querySelector('.js-pipelines-tab-finished .badge').textContent.trim(), - ).toContain(data.count.finished); - }); - - it('should not render badge when number is undefined', () => { - vm = mountComponent(Component, { - scope: 'all', - paths: {}, - count: {}, - }); - - // All - expect( - vm.$el.querySelector('.js-totalbuilds-count'), - ).toEqual(null); - - // Pending - expect( - vm.$el.querySelector('.js-pipelines-tab-pending .badge'), - ).toEqual(null); - - // Running - expect( - vm.$el.querySelector('.js-pipelines-tab-running .badge'), - ).toEqual(null); - - // Finished - expect( - vm.$el.querySelector('.js-pipelines-tab-finished .badge'), - ).toEqual(null); - }); - }); -}); diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index c30abb2edb0..367b42cefb0 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines', () => { const jsonFixtureName = 'pipelines/pipelines.json'; @@ -9,26 +10,33 @@ describe('Pipelines', () => { preloadFixtures(jsonFixtureName); let PipelinesComponent; - let pipeline; + let pipelines; + let component; beforeEach(() => { loadFixtures('static/pipelines.html.raw'); - const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + pipelines = getJSONFixture(jsonFixtureName); PipelinesComponent = Vue.extend(pipelinesComp); }); + afterEach(() => { + component.$destroy(); + }); + describe('successfull request', () => { describe('with pipelines', () => { const pipelinesInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(pipeline), { + next(request.respondWith(JSON.stringify(pipelines), { status: 200, })); }; beforeEach(() => { Vue.http.interceptors.push(pipelinesInterceptor); + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); }); afterEach(() => { @@ -38,18 +46,71 @@ describe('Pipelines', () => { }); it('should render table', (done) => { - const component = new PipelinesComponent({ - propsData: { - store: new Store(), - }, - }).$mount(); - setTimeout(() => { expect(component.$el.querySelector('.table-holder')).toBeDefined(); - expect(component.$el.querySelector('.realtime-loading')).toBe(null); + expect( + component.$el.querySelectorAll('.gl-responsive-table-row').length, + ).toEqual(pipelines.pipelines.length + 1); + done(); + }); + }); + + it('should render navigation tabs', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), + ).toContain('Pending'); + expect( + component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), + ).toContain('All'); + expect( + component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), + ).toContain('Running'); + expect( + component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), + ).toContain('Finished'); + expect( + component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), + ).toContain('Branches'); + expect( + component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), + ).toContain('Tags'); + done(); + }); + }); + + it('should make an API request when using tabs', (done) => { + setTimeout(() => { + spyOn(component, 'updateContent'); + component.$el.querySelector('.js-pipelines-tab-finished').click(); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); done(); }); }); + + describe('with pagination', () => { + it('should make an API request when using pagination', (done) => { + setTimeout(() => { + spyOn(component, 'updateContent'); + // Mock pagination + component.store.state.pageInfo = { + page: 1, + total: 10, + perPage: 2, + nextPage: 2, + totalPages: 5, + }; + + Vue.nextTick(() => { + component.$el.querySelector('.js-next-button a').click(); + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); + + done(); + }); + }); + }); + }); }); describe('without pipelines', () => { @@ -70,15 +131,14 @@ describe('Pipelines', () => { }); it('should render empty state', (done) => { - const component = new PipelinesComponent({ + component = new PipelinesComponent({ propsData: { store: new Store(), }, }).$mount(); setTimeout(() => { - expect(component.$el.querySelector('.empty-state')).toBeDefined(); - expect(component.$el.querySelector('.realtime-loading')).toBe(null); + expect(component.$el.querySelector('.empty-state')).not.toBe(null); done(); }); }); @@ -103,7 +163,7 @@ describe('Pipelines', () => { }); it('should render error state', (done) => { - const component = new PipelinesComponent({ + component = new PipelinesComponent({ propsData: { store: new Store(), }, @@ -111,9 +171,54 @@ describe('Pipelines', () => { setTimeout(() => { expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); - expect(component.$el.querySelector('.realtime-loading')).toBe(null); done(); }); }); }); + + describe('methods', () => { + beforeEach(() => { + spyOn(history, 'pushState').and.stub(); + }); + + describe('updateContent', () => { + it('should set given parameters', () => { + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); + component.updateContent({ scope: 'finished', page: '4' }); + + expect(component.page).toEqual('4'); + expect(component.scope).toEqual('finished'); + expect(component.requestData.scope).toEqual('finished'); + expect(component.requestData.page).toEqual('4'); + }); + }); + + describe('onChangeTab', () => { + it('should set page to 1', () => { + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); + spyOn(component, 'updateContent'); + + component.onChangeTab('running'); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + }); + }); + + describe('onChangePage', () => { + it('should update page and keep scope', () => { + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); + spyOn(component, 'updateContent'); + + component.onChangePage(4); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); + }); + }); + }); }); diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js index 171629fcd6b..edef150dd1e 100644 --- a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js +++ b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js @@ -50,6 +50,18 @@ describe('ProjectsListItemComponent', () => { expect(vm.highlightedProjectName).toBe(mockProject.name); }); }); + + describe('truncatedNamespace', () => { + it('should truncate project name from namespace string', () => { + vm.namespace = 'platform / nokia-3310'; + expect(vm.truncatedNamespace).toBe('platform'); + }); + + it('should truncate namespace string from the middle if it includes more than two groups in path', () => { + vm.namespace = 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310'; + expect(vm.truncatedNamespace).toBe('platform / ... / Mobile Chipset'); + }); + }); }); describe('template', () => { diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js new file mode 100644 index 00000000000..f750061a6a1 --- /dev/null +++ b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import store from '~/repo/stores'; +import listCollapsed from '~/repo/components/commit_sidebar/list_collapsed.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file } from '../../helpers'; + +describe('Multi-file editor commit sidebar list collapsed', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(listCollapsed); + + vm = createComponentWithStore(Component, store); + + vm.$store.state.openFiles.push(file(), file()); + vm.$store.state.openFiles[0].tempFile = true; + vm.$store.state.openFiles.forEach((f) => { + Object.assign(f, { + changed: true, + }); + }); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders added & modified files count', () => { + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1'); + }); +}); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js new file mode 100644 index 00000000000..18c9b46fcd9 --- /dev/null +++ b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js @@ -0,0 +1,53 @@ +import Vue from 'vue'; +import listItem from '~/repo/components/commit_sidebar/list_item.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; +import { file } from '../../helpers'; + +describe('Multi-file editor commit sidebar list item', () => { + let vm; + let f; + + beforeEach(() => { + const Component = Vue.extend(listItem); + + f = file(); + + vm = mountComponent(Component, { + file: f, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders file path', () => { + expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path); + }); + + describe('computed', () => { + describe('iconName', () => { + it('returns modified when not a tempFile', () => { + expect(vm.iconName).toBe('file-modified'); + }); + + it('returns addition when not a tempFile', () => { + f.tempFile = true; + + expect(vm.iconName).toBe('file-addition'); + }); + }); + + describe('iconClass', () => { + it('returns modified when not a tempFile', () => { + expect(vm.iconClass).toContain('multi-file-modified'); + }); + + it('returns addition when not a tempFile', () => { + f.tempFile = true; + + expect(vm.iconClass).toContain('multi-file-addition'); + }); + }); + }); +}); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_spec.js new file mode 100644 index 00000000000..df7e3c5de21 --- /dev/null +++ b/spec/javascripts/repo/components/commit_sidebar/list_spec.js @@ -0,0 +1,72 @@ +import Vue from 'vue'; +import store from '~/repo/stores'; +import commitSidebarList from '~/repo/components/commit_sidebar/list.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file } from '../../helpers'; + +describe('Multi-file editor commit sidebar list', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(commitSidebarList); + + vm = createComponentWithStore(Component, store, { + title: 'Staged', + fileList: [], + collapsed: false, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('empty file list', () => { + it('renders no changes text', () => { + expect(vm.$el.querySelector('.help-block').textContent.trim()).toBe('No changes'); + }); + }); + + describe('with a list of files', () => { + beforeEach((done) => { + const f = file('file name'); + f.changed = true; + vm.fileList.push(f); + + Vue.nextTick(done); + }); + + it('renders list', () => { + expect(vm.$el.querySelectorAll('li').length).toBe(1); + }); + }); + + describe('collapsed', () => { + beforeEach((done) => { + vm.collapsed = true; + + Vue.nextTick(done); + }); + + it('adds collapsed class', () => { + expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull(); + }); + + it('hides list', () => { + expect(vm.$el.querySelector('.list-unstyled')).toBeNull(); + expect(vm.$el.querySelector('.help-block')).toBeNull(); + }); + + it('hides collapse button', () => { + expect(vm.$el.querySelector('.multi-file-commit-panel-collapse-btn')).toBeNull(); + }); + }); + + it('clicking toggle collapse button emits toggle event', () => { + spyOn(vm, '$emit'); + + vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); + + expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed'); + }); +}); diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index 0f991e1b727..1c794123095 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -25,8 +25,12 @@ describe('RepoCommitSection', () => { return comp.$mount(); } - beforeEach(() => { + beforeEach((done) => { vm = createComponent(); + + vm.collapsed = false; + + Vue.nextTick(done); }); afterEach(() => { @@ -36,12 +40,11 @@ describe('RepoCommitSection', () => { }); it('renders a commit section', () => { - const changedFileElements = [...vm.$el.querySelectorAll('.changed-files > li')]; - const submitCommit = vm.$el.querySelector('.btn'); - const targetBranch = vm.$el.querySelector('.target-branch'); + const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')]; + const submitCommit = vm.$el.querySelector('form .btn'); - expect(vm.$el.querySelector(':scope > form')).toBeTruthy(); - expect(vm.$el.querySelector('.staged-files').textContent.trim()).toEqual('Staged files (2)'); + expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull(); + expect(vm.$el.querySelector('.multi-file-commit-panel-section header').textContent.trim()).toEqual('Staged'); expect(changedFileElements.length).toEqual(2); changedFileElements.forEach((changedFile, i) => { @@ -49,10 +52,7 @@ describe('RepoCommitSection', () => { }); expect(submitCommit.disabled).toBeTruthy(); - expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy(); - expect(vm.$el.querySelector('.commit-summary').textContent.trim()).toEqual('Commit 2 files'); - expect(targetBranch.querySelector(':scope > label').textContent.trim()).toEqual('Target branch'); - expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual('master'); + expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull(); }); describe('when submitting', () => { @@ -69,7 +69,7 @@ describe('RepoCommitSection', () => { }); it('allows you to submit', () => { - expect(vm.$el.querySelector('.btn').disabled).toBeTruthy(); + expect(vm.$el.querySelector('form .btn').disabled).toBeTruthy(); }); it('submits commit', (done) => { diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js index 979d2185076..81158cad639 100644 --- a/spec/javascripts/repo/components/repo_editor_spec.js +++ b/spec/javascripts/repo/components/repo_editor_spec.js @@ -1,12 +1,13 @@ import Vue from 'vue'; import store from '~/repo/stores'; import repoEditor from '~/repo/components/repo_editor.vue'; +import monacoLoader from '~/repo/monaco_loader'; import { file, resetStore } from '../helpers'; describe('RepoEditor', () => { let vm; - beforeEach(() => { + beforeEach((done) => { const f = file(); const RepoEditor = Vue.extend(repoEditor); @@ -21,6 +22,10 @@ describe('RepoEditor', () => { vm.monaco = true; vm.$mount(); + + monacoLoader(['vs/editor/editor.main'], () => { + setTimeout(done, 0); + }); }); afterEach(() => { @@ -32,7 +37,6 @@ describe('RepoEditor', () => { it('renders an ide container', (done) => { Vue.nextTick(() => { expect(vm.shouldHideEditor).toBeFalsy(); - expect(vm.$el.textContent.trim()).toBe(''); done(); }); @@ -50,7 +54,7 @@ describe('RepoEditor', () => { }); it('shows activeFile html', () => { - expect(vm.$el.textContent.trim()).toBe('testing'); + expect(vm.$el.textContent).toContain('testing'); }); }); }); diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index 7cb4dace491..df7cf8aabbb 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -29,7 +29,6 @@ describe('RepoSidebar', () => { const thead = vm.$el.querySelector('thead'); const tbody = vm.$el.querySelector('tbody'); - expect(vm.$el.id).toEqual('sidebar'); expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy(); expect(thead.querySelector('.name').textContent.trim()).toEqual('Name'); expect(thead.querySelector('.last-commit').textContent.trim()).toEqual('Last commit'); @@ -40,18 +39,6 @@ describe('RepoSidebar', () => { expect(tbody.querySelector('.file')).toBeTruthy(); }); - it('does not render a thead, renders repo-file-options and sets sidebar-mini class if isMini', (done) => { - vm.$store.state.openFiles.push(vm.$store.state.tree[0]); - - Vue.nextTick(() => { - expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy(); - expect(vm.$el.querySelector('thead')).toBeTruthy(); - expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy(); - - done(); - }); - }); - it('renders 5 loading files if tree is loading', (done) => { vm.$store.state.tree = []; vm.$store.state.loading = true; diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js index df0ca55aafc..7d2174196c9 100644 --- a/spec/javascripts/repo/components/repo_tab_spec.js +++ b/spec/javascripts/repo/components/repo_tab_spec.js @@ -24,8 +24,8 @@ describe('RepoTab', () => { tab: file(), }); vm.$store.state.openFiles.push(vm.tab); - const close = vm.$el.querySelector('.close-btn'); - const name = vm.$el.querySelector(`a[title="${vm.tab.url}"]`); + const close = vm.$el.querySelector('.multi-file-tab-close'); + const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`); expect(close.querySelector('.fa-times')).toBeTruthy(); expect(name.textContent.trim()).toEqual(vm.tab.name); @@ -50,7 +50,7 @@ describe('RepoTab', () => { spyOn(vm, 'closeFile'); - vm.$el.querySelector('.close-btn').click(); + vm.$el.querySelector('.multi-file-tab-close').click(); expect(vm.closeFile).toHaveBeenCalledWith({ file: vm.tab }); }); @@ -62,7 +62,7 @@ describe('RepoTab', () => { tab, }); - expect(vm.$el.querySelector('.close-btn .fa-circle')).toBeTruthy(); + expect(vm.$el.querySelector('.multi-file-tab-close .fa-circle')).not.toBeNull(); }); describe('methods', () => { @@ -77,7 +77,7 @@ describe('RepoTab', () => { vm.$store.state.openFiles.push(tab); vm.$store.dispatch('setFileActive', tab); - vm.$el.querySelector('.close-btn').click(); + vm.$el.querySelector('.multi-file-tab-close').click(); vm.$nextTick(() => { expect(tab.opened).toBeTruthy(); @@ -95,7 +95,7 @@ describe('RepoTab', () => { vm.$store.state.openFiles.push(tab); vm.$store.dispatch('setFileActive', tab); - vm.$el.querySelector('.close-btn').click(); + vm.$el.querySelector('.multi-file-tab-close').click(); vm.$nextTick(() => { expect(tab.opened).toBeFalsy(); diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js index d0246cc72e6..1fb2242c051 100644 --- a/spec/javascripts/repo/components/repo_tabs_spec.js +++ b/spec/javascripts/repo/components/repo_tabs_spec.js @@ -25,12 +25,11 @@ describe('RepoTabs', () => { vm.$store.state.openFiles = openedFiles; vm.$nextTick(() => { - const tabs = [...vm.$el.querySelectorAll(':scope > li')]; + const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')]; - expect(tabs.length).toEqual(3); + expect(tabs.length).toEqual(2); expect(tabs[0].classList.contains('active')).toBeTruthy(); expect(tabs[1].classList.contains('active')).toBeFalsy(); - expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy(); done(); }); diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js index 376c291c64b..820a44992b4 100644 --- a/spec/javascripts/repo/helpers.js +++ b/spec/javascripts/repo/helpers.js @@ -12,9 +12,4 @@ export const file = (name = 'name', id = name, type = '') => decorateData({ url: 'url', name, path: name, - last_commit: { - id: '123', - message: 'test', - committed_date: new Date().toISOString(), - }, }); diff --git a/spec/javascripts/repo/lib/common/disposable_spec.js b/spec/javascripts/repo/lib/common/disposable_spec.js new file mode 100644 index 00000000000..62c3913bf4d --- /dev/null +++ b/spec/javascripts/repo/lib/common/disposable_spec.js @@ -0,0 +1,44 @@ +import Disposable from '~/repo/lib/common/disposable'; + +describe('Multi-file editor library disposable class', () => { + let instance; + let disposableClass; + + beforeEach(() => { + instance = new Disposable(); + + disposableClass = { + dispose: jasmine.createSpy('dispose'), + }; + }); + + afterEach(() => { + instance.dispose(); + }); + + describe('add', () => { + it('adds disposable classes', () => { + instance.add(disposableClass); + + expect(instance.disposers.size).toBe(1); + }); + }); + + describe('dispose', () => { + beforeEach(() => { + instance.add(disposableClass); + }); + + it('calls dispose on all cached disposers', () => { + instance.dispose(); + + expect(disposableClass.dispose).toHaveBeenCalled(); + }); + + it('clears cached disposers', () => { + instance.dispose(); + + expect(instance.disposers.size).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/common/model_manager_spec.js b/spec/javascripts/repo/lib/common/model_manager_spec.js new file mode 100644 index 00000000000..8c134f178c0 --- /dev/null +++ b/spec/javascripts/repo/lib/common/model_manager_spec.js @@ -0,0 +1,81 @@ +/* global monaco */ +import monacoLoader from '~/repo/monaco_loader'; +import ModelManager from '~/repo/lib/common/model_manager'; +import { file } from '../../helpers'; + +describe('Multi-file editor library model manager', () => { + let instance; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + instance = new ModelManager(monaco); + + done(); + }); + }); + + afterEach(() => { + instance.dispose(); + }); + + describe('addModel', () => { + it('caches model', () => { + instance.addModel(file()); + + expect(instance.models.size).toBe(1); + }); + + it('caches model by file path', () => { + instance.addModel(file('path-name')); + + expect(instance.models.keys().next().value).toBe('path-name'); + }); + + it('adds model into disposable', () => { + spyOn(instance.disposable, 'add').and.callThrough(); + + instance.addModel(file()); + + expect(instance.disposable.add).toHaveBeenCalled(); + }); + + it('returns cached model', () => { + spyOn(instance.models, 'get').and.callThrough(); + + instance.addModel(file()); + instance.addModel(file()); + + expect(instance.models.get).toHaveBeenCalled(); + }); + }); + + describe('hasCachedModel', () => { + it('returns false when no models exist', () => { + expect(instance.hasCachedModel('path')).toBeFalsy(); + }); + + it('returns true when model exists', () => { + instance.addModel(file('path-name')); + + expect(instance.hasCachedModel('path-name')).toBeTruthy(); + }); + }); + + describe('dispose', () => { + it('clears cached models', () => { + instance.addModel(file()); + + instance.dispose(); + + expect(instance.models.size).toBe(0); + }); + + it('calls disposable dispose', () => { + spyOn(instance.disposable, 'dispose').and.callThrough(); + + instance.dispose(); + + expect(instance.disposable.dispose).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/common/model_spec.js b/spec/javascripts/repo/lib/common/model_spec.js new file mode 100644 index 00000000000..d41ade237ca --- /dev/null +++ b/spec/javascripts/repo/lib/common/model_spec.js @@ -0,0 +1,84 @@ +/* global monaco */ +import monacoLoader from '~/repo/monaco_loader'; +import Model from '~/repo/lib/common/model'; +import { file } from '../../helpers'; + +describe('Multi-file editor library model', () => { + let model; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + model = new Model(monaco, file('path')); + + done(); + }); + }); + + afterEach(() => { + model.dispose(); + }); + + it('creates original model & new model', () => { + expect(model.originalModel).not.toBeNull(); + expect(model.model).not.toBeNull(); + }); + + describe('path', () => { + it('returns file path', () => { + expect(model.path).toBe('path'); + }); + }); + + describe('getModel', () => { + it('returns model', () => { + expect(model.getModel()).toBe(model.model); + }); + }); + + describe('getOriginalModel', () => { + it('returns original model', () => { + expect(model.getOriginalModel()).toBe(model.originalModel); + }); + }); + + describe('onChange', () => { + it('caches event by path', () => { + model.onChange(() => {}); + + expect(model.events.size).toBe(1); + expect(model.events.keys().next().value).toBe('path'); + }); + + it('calls callback on change', (done) => { + const spy = jasmine.createSpy(); + model.onChange(spy); + + model.getModel().setValue('123'); + + setTimeout(() => { + expect(spy).toHaveBeenCalledWith(model.getModel(), jasmine.anything()); + done(); + }); + }); + }); + + describe('dispose', () => { + it('calls disposable dispose', () => { + spyOn(model.disposable, 'dispose').and.callThrough(); + + model.dispose(); + + expect(model.disposable.dispose).toHaveBeenCalled(); + }); + + it('clears events', () => { + model.onChange(() => {}); + + expect(model.events.size).toBe(1); + + model.dispose(); + + expect(model.events.size).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/decorations/controller_spec.js b/spec/javascripts/repo/lib/decorations/controller_spec.js new file mode 100644 index 00000000000..2e32e8fa0bd --- /dev/null +++ b/spec/javascripts/repo/lib/decorations/controller_spec.js @@ -0,0 +1,120 @@ +/* global monaco */ +import monacoLoader from '~/repo/monaco_loader'; +import editor from '~/repo/lib/editor'; +import DecorationsController from '~/repo/lib/decorations/controller'; +import Model from '~/repo/lib/common/model'; +import { file } from '../../helpers'; + +describe('Multi-file editor library decorations controller', () => { + let editorInstance; + let controller; + let model; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + editorInstance = editor.create(monaco); + editorInstance.createInstance(document.createElement('div')); + + controller = new DecorationsController(editorInstance); + model = new Model(monaco, file('path')); + + done(); + }); + }); + + afterEach(() => { + model.dispose(); + editorInstance.dispose(); + controller.dispose(); + }); + + describe('getAllDecorationsForModel', () => { + it('returns empty array when no decorations exist for model', () => { + const decorations = controller.getAllDecorationsForModel(model); + + expect(decorations).toEqual([]); + }); + + it('returns decorations by model URL', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + const decorations = controller.getAllDecorationsForModel(model); + + expect(decorations[0]).toEqual({ decoration: 'decorationValue' }); + }); + }); + + describe('addDecorations', () => { + it('caches decorations in a new map', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.decorations.size).toBe(1); + }); + + it('does not create new cache model', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]); + + expect(controller.decorations.size).toBe(1); + }); + + it('caches decorations by model URL', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.decorations.size).toBe(1); + expect(controller.decorations.keys().next().value).toBe('path'); + }); + + it('calls decorate method', () => { + spyOn(controller, 'decorate'); + + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.decorate).toHaveBeenCalled(); + }); + }); + + describe('decorate', () => { + it('sets decorations on editor instance', () => { + spyOn(controller.editor.instance, 'deltaDecorations'); + + controller.decorate(model); + + expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []); + }); + + it('caches decorations', () => { + spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]); + + controller.decorate(model); + + expect(controller.editorDecorations.size).toBe(1); + }); + + it('caches decorations by model URL', () => { + spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]); + + controller.decorate(model); + + expect(controller.editorDecorations.keys().next().value).toBe('path'); + }); + }); + + describe('dispose', () => { + it('clears cached decorations', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + controller.dispose(); + + expect(controller.decorations.size).toBe(0); + }); + + it('clears cached editorDecorations', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + controller.dispose(); + + expect(controller.editorDecorations.size).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/diff/controller_spec.js b/spec/javascripts/repo/lib/diff/controller_spec.js new file mode 100644 index 00000000000..ed62e28d3a3 --- /dev/null +++ b/spec/javascripts/repo/lib/diff/controller_spec.js @@ -0,0 +1,176 @@ +/* global monaco */ +import monacoLoader from '~/repo/monaco_loader'; +import editor from '~/repo/lib/editor'; +import ModelManager from '~/repo/lib/common/model_manager'; +import DecorationsController from '~/repo/lib/decorations/controller'; +import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/repo/lib/diff/controller'; +import { computeDiff } from '~/repo/lib/diff/diff'; +import { file } from '../../helpers'; + +describe('Multi-file editor library dirty diff controller', () => { + let editorInstance; + let controller; + let modelManager; + let decorationsController; + let model; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + editorInstance = editor.create(monaco); + editorInstance.createInstance(document.createElement('div')); + + modelManager = new ModelManager(monaco); + decorationsController = new DecorationsController(editorInstance); + + model = modelManager.addModel(file()); + + controller = new DirtyDiffController(modelManager, decorationsController); + + done(); + }); + }); + + afterEach(() => { + controller.dispose(); + model.dispose(); + decorationsController.dispose(); + editorInstance.dispose(); + }); + + describe('getDiffChangeType', () => { + ['added', 'removed', 'modified'].forEach((type) => { + it(`returns ${type}`, () => { + const change = { + [type]: true, + }; + + expect(getDiffChangeType(change)).toBe(type); + }); + }); + }); + + describe('getDecorator', () => { + ['added', 'removed', 'modified'].forEach((type) => { + it(`returns with linesDecorationsClassName for ${type}`, () => { + const change = { + [type]: true, + }; + + expect( + getDecorator(change).options.linesDecorationsClassName, + ).toBe(`dirty-diff dirty-diff-${type}`); + }); + + it('returns with line numbers', () => { + const change = { + lineNumber: 1, + endLineNumber: 2, + [type]: true, + }; + + const range = getDecorator(change).range; + + expect(range.startLineNumber).toBe(1); + expect(range.endLineNumber).toBe(2); + expect(range.startColumn).toBe(1); + expect(range.endColumn).toBe(1); + }); + }); + }); + + describe('attachModel', () => { + it('adds change event callback', () => { + spyOn(model, 'onChange'); + + controller.attachModel(model); + + expect(model.onChange).toHaveBeenCalled(); + }); + + it('calls throttledComputeDiff on change', () => { + spyOn(controller, 'throttledComputeDiff'); + + controller.attachModel(model); + + model.getModel().setValue('123'); + + expect(controller.throttledComputeDiff).toHaveBeenCalled(); + }); + }); + + describe('computeDiff', () => { + it('posts to worker', () => { + spyOn(controller.dirtyDiffWorker, 'postMessage'); + + controller.computeDiff(model); + + expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({ + path: model.path, + originalContent: '', + newContent: '', + }); + }); + }); + + describe('reDecorate', () => { + it('calls decorations controller decorate', () => { + spyOn(controller.decorationsController, 'decorate'); + + controller.reDecorate(model); + + expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model); + }); + }); + + describe('decorate', () => { + it('adds decorations into decorations controller', () => { + spyOn(controller.decorationsController, 'addDecorations'); + + controller.decorate({ data: { changes: [], path: 'path' } }); + + expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith('path', 'dirtyDiff', jasmine.anything()); + }); + + it('adds decorations into editor', () => { + const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations'); + + controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } }); + + expect(spy).toHaveBeenCalledWith([], [{ + range: new monaco.Range( + 1, 1, 1, 1, + ), + options: { + isWholeLine: true, + linesDecorationsClassName: 'dirty-diff dirty-diff-modified', + }, + }]); + }); + }); + + describe('dispose', () => { + it('calls disposable dispose', () => { + spyOn(controller.disposable, 'dispose').and.callThrough(); + + controller.dispose(); + + expect(controller.disposable.dispose).toHaveBeenCalled(); + }); + + it('terminates worker', () => { + spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough(); + + controller.dispose(); + + expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled(); + }); + + it('removes worker event listener', () => { + spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough(); + + controller.dispose(); + + expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything()); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js new file mode 100644 index 00000000000..3269ec5d2c9 --- /dev/null +++ b/spec/javascripts/repo/lib/diff/diff_spec.js @@ -0,0 +1,80 @@ +import { computeDiff } from '~/repo/lib/diff/diff'; + +describe('Multi-file editor library diff calculator', () => { + describe('computeDiff', () => { + it('returns empty array if no changes', () => { + const diff = computeDiff('123', '123'); + + expect(diff).toEqual([]); + }); + + describe('modified', () => { + it('', () => { + const diff = computeDiff('123', '1234')[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeUndefined(); + }); + + it('', () => { + const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeUndefined(); + expect(diff.lineNumber).toBe(2); + }); + }); + + describe('added', () => { + it('', () => { + const diff = computeDiff('123', '123\n123')[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeUndefined(); + }); + + it('', () => { + const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeUndefined(); + expect(diff.lineNumber).toBe(3); + }); + }); + + describe('removed', () => { + it('', () => { + const diff = computeDiff('123', '')[0]; + + expect(diff.added).toBeUndefined(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeTruthy(); + }); + + it('', () => { + const diff = computeDiff('123\n123\n123', '123\n123')[0]; + + expect(diff.added).toBeUndefined(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeTruthy(); + expect(diff.lineNumber).toBe(2); + }); + }); + + it('includes line number of change', () => { + const diff = computeDiff('123', '')[0]; + + expect(diff.lineNumber).toBe(1); + }); + + it('includes end line number of change', () => { + const diff = computeDiff('123', '')[0]; + + expect(diff.endLineNumber).toBe(1); + }); + }); +}); diff --git a/spec/javascripts/repo/lib/editor_options_spec.js b/spec/javascripts/repo/lib/editor_options_spec.js new file mode 100644 index 00000000000..b4887d063ed --- /dev/null +++ b/spec/javascripts/repo/lib/editor_options_spec.js @@ -0,0 +1,7 @@ +import editorOptions from '~/repo/lib/editor_options'; + +describe('Multi-file editor library editor options', () => { + it('returns an array', () => { + expect(editorOptions).toEqual(jasmine.any(Array)); + }); +}); diff --git a/spec/javascripts/repo/lib/editor_spec.js b/spec/javascripts/repo/lib/editor_spec.js new file mode 100644 index 00000000000..cd32832a232 --- /dev/null +++ b/spec/javascripts/repo/lib/editor_spec.js @@ -0,0 +1,128 @@ +/* global monaco */ +import monacoLoader from '~/repo/monaco_loader'; +import editor from '~/repo/lib/editor'; +import { file } from '../helpers'; + +describe('Multi-file editor library', () => { + let instance; + + beforeEach((done) => { + monacoLoader(['vs/editor/editor.main'], () => { + instance = editor.create(monaco); + + done(); + }); + }); + + afterEach(() => { + instance.dispose(); + }); + + it('creates instance of editor', () => { + expect(editor.editorInstance).not.toBeNull(); + }); + + describe('createInstance', () => { + let el; + + beforeEach(() => { + el = document.createElement('div'); + }); + + it('creates editor instance', () => { + spyOn(instance.monaco.editor, 'create').and.callThrough(); + + instance.createInstance(el); + + expect(instance.monaco.editor.create).toHaveBeenCalled(); + }); + + it('creates dirty diff controller', () => { + instance.createInstance(el); + + expect(instance.dirtyDiffController).not.toBeNull(); + }); + }); + + describe('createModel', () => { + it('calls model manager addModel', () => { + spyOn(instance.modelManager, 'addModel'); + + instance.createModel('FILE'); + + expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE'); + }); + }); + + describe('attachModel', () => { + let model; + + beforeEach(() => { + instance.createInstance(document.createElement('div')); + + model = instance.createModel(file()); + }); + + it('sets the current model on the instance', () => { + instance.attachModel(model); + + expect(instance.currentModel).toBe(model); + }); + + it('attaches the model to the current instance', () => { + spyOn(instance.instance, 'setModel'); + + instance.attachModel(model); + + expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel()); + }); + + it('attaches the model to the dirty diff controller', () => { + spyOn(instance.dirtyDiffController, 'attachModel'); + + instance.attachModel(model); + + expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model); + }); + + it('re-decorates with the dirty diff controller', () => { + spyOn(instance.dirtyDiffController, 'reDecorate'); + + instance.attachModel(model); + + expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model); + }); + }); + + describe('clearEditor', () => { + it('resets the editor model', () => { + instance.createInstance(document.createElement('div')); + + spyOn(instance.instance, 'setModel'); + + instance.clearEditor(); + + expect(instance.instance.setModel).toHaveBeenCalledWith(null); + }); + }); + + describe('dispose', () => { + it('calls disposble dispose method', () => { + spyOn(instance.disposable, 'dispose').and.callThrough(); + + instance.dispose(); + + expect(instance.disposable.dispose).toHaveBeenCalled(); + }); + + it('resets instance', () => { + instance.createInstance(document.createElement('div')); + + expect(instance.instance).not.toBeNull(); + + instance.dispose(); + + expect(instance.instance).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js new file mode 100644 index 00000000000..af9d6835a67 --- /dev/null +++ b/spec/javascripts/repo/stores/actions/branch_spec.js @@ -0,0 +1,38 @@ +import store from '~/repo/stores'; +import service from '~/repo/services'; +import { resetStore } from '../../helpers'; + +describe('Multi-file store branch actions', () => { + afterEach(() => { + resetStore(store); + }); + + describe('createNewBranch', () => { + beforeEach(() => { + spyOn(service, 'createBranch').and.returnValue(Promise.resolve({ + json: () => ({ + name: 'testing', + }), + })); + spyOn(history, 'pushState'); + + store.state.project.id = 2; + store.state.currentBranch = 'testing'; + }); + + it('creates new branch', (done) => { + store.dispatch('createNewBranch', 'master') + .then(() => { + expect(store.state.currentBranch).toBe('testing'); + expect(service.createBranch).toHaveBeenCalledWith(2, { + branch: 'master', + ref: 'testing', + }); + expect(history.pushState).toHaveBeenCalled(); + + done(); + }) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js new file mode 100644 index 00000000000..099c0556e71 --- /dev/null +++ b/spec/javascripts/repo/stores/actions/file_spec.js @@ -0,0 +1,417 @@ +import Vue from 'vue'; +import store from '~/repo/stores'; +import service from '~/repo/services'; +import { file, resetStore } from '../../helpers'; + +describe('Multi-file store file actions', () => { + afterEach(() => { + resetStore(store); + }); + + describe('closeFile', () => { + let localFile; + let getLastCommitDataSpy; + let oldGetLastCommitData; + + beforeEach(() => { + getLastCommitDataSpy = jasmine.createSpy('getLastCommitData'); + oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line + store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line + + localFile = file(); + localFile.active = true; + localFile.opened = true; + localFile.parentTreeUrl = 'parentTreeUrl'; + + store.state.openFiles.push(localFile); + + spyOn(history, 'pushState'); + }); + + afterEach(() => { + store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line + }); + + it('closes open files', (done) => { + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(localFile.opened).toBeFalsy(); + expect(localFile.active).toBeFalsy(); + expect(store.state.openFiles.length).toBe(0); + + done(); + }).catch(done.fail); + }); + + it('does not close file if has changed', (done) => { + localFile.changed = true; + + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(localFile.opened).toBeTruthy(); + expect(localFile.active).toBeTruthy(); + expect(store.state.openFiles.length).toBe(1); + + done(); + }).catch(done.fail); + }); + + it('does not close file if temp file', (done) => { + localFile.tempFile = true; + + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(localFile.opened).toBeTruthy(); + expect(localFile.active).toBeTruthy(); + expect(store.state.openFiles.length).toBe(1); + + done(); + }).catch(done.fail); + }); + + it('force closes a changed file', (done) => { + localFile.changed = true; + + store.dispatch('closeFile', { file: localFile, force: true }) + .then(() => { + expect(localFile.opened).toBeFalsy(); + expect(localFile.active).toBeFalsy(); + expect(store.state.openFiles.length).toBe(0); + + done(); + }).catch(done.fail); + }); + + it('calls pushState when no open files are left', (done) => { + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'parentTreeUrl'); + + done(); + }).catch(done.fail); + }); + + it('sets next file as active', (done) => { + const f = file(); + store.state.openFiles.push(f); + + expect(f.active).toBeFalsy(); + + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(f.active).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('calls getLastCommitData', (done) => { + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(getLastCommitDataSpy).toHaveBeenCalled(); + + done(); + }).catch(done.fail); + }); + }); + + describe('setFileActive', () => { + let scrollToTabSpy; + let oldScrollToTab; + + beforeEach(() => { + scrollToTabSpy = jasmine.createSpy('scrollToTab'); + oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line + store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line + }); + + afterEach(() => { + store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line + }); + + it('calls scrollToTab', (done) => { + store.dispatch('setFileActive', file()) + .then(() => { + expect(scrollToTabSpy).toHaveBeenCalled(); + + done(); + }).catch(done.fail); + }); + + it('sets the file active', (done) => { + const localFile = file(); + + store.dispatch('setFileActive', localFile) + .then(() => { + expect(localFile.active).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('returns early if file is already active', (done) => { + const localFile = file(); + localFile.active = true; + + store.dispatch('setFileActive', localFile) + .then(() => { + expect(scrollToTabSpy).not.toHaveBeenCalled(); + + done(); + }).catch(done.fail); + }); + + it('sets current active file to not active', (done) => { + const localFile = file(); + localFile.active = true; + store.state.openFiles.push(localFile); + + store.dispatch('setFileActive', file()) + .then(() => { + expect(localFile.active).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('resets location.hash for line highlighting', (done) => { + location.hash = 'test'; + + store.dispatch('setFileActive', file()) + .then(() => { + expect(location.hash).not.toBe('test'); + + done(); + }).catch(done.fail); + }); + }); + + describe('getFileData', () => { + let localFile = file(); + + beforeEach(() => { + spyOn(service, 'getFileData').and.returnValue(Promise.resolve({ + headers: { + 'page-title': 'testing getFileData', + }, + json: () => Promise.resolve({ + blame_path: 'blame_path', + commits_path: 'commits_path', + permalink: 'permalink', + raw_path: 'raw_path', + binary: false, + html: '123', + render_error: '', + }), + })); + + localFile = file(); + localFile.url = 'getFileDataURL'; + }); + + it('calls the service', (done) => { + store.dispatch('getFileData', localFile) + .then(() => { + expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL'); + + done(); + }).catch(done.fail); + }); + + it('sets the file data', (done) => { + store.dispatch('getFileData', localFile) + .then(Vue.nextTick) + .then(() => { + expect(localFile.blamePath).toBe('blame_path'); + + done(); + }).catch(done.fail); + }); + + it('sets document title', (done) => { + store.dispatch('getFileData', localFile) + .then(() => { + expect(document.title).toBe('testing getFileData'); + + done(); + }).catch(done.fail); + }); + + it('sets the file as active', (done) => { + store.dispatch('getFileData', localFile) + .then(Vue.nextTick) + .then(() => { + expect(localFile.active).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('adds the file to open files', (done) => { + store.dispatch('getFileData', localFile) + .then(Vue.nextTick) + .then(() => { + expect(store.state.openFiles.length).toBe(1); + expect(store.state.openFiles[0].name).toBe(localFile.name); + + done(); + }).catch(done.fail); + }); + + it('toggles the file loading', (done) => { + store.dispatch('getFileData', localFile) + .then(() => { + expect(localFile.loading).toBeTruthy(); + + return Vue.nextTick(); + }) + .then(() => { + expect(localFile.loading).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + }); + + describe('getRawFileData', () => { + let tmpFile; + + beforeEach(() => { + spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw')); + + tmpFile = file(); + }); + + it('calls getRawFileData service method', (done) => { + store.dispatch('getRawFileData', tmpFile) + .then(() => { + expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); + + done(); + }).catch(done.fail); + }); + + it('updates file raw data', (done) => { + store.dispatch('getRawFileData', tmpFile) + .then(() => { + expect(tmpFile.raw).toBe('raw'); + + done(); + }).catch(done.fail); + }); + }); + + describe('changeFileContent', () => { + let tmpFile; + + beforeEach(() => { + tmpFile = file(); + }); + + it('updates file content', (done) => { + store.dispatch('changeFileContent', { + file: tmpFile, + content: 'content', + }) + .then(() => { + expect(tmpFile.content).toBe('content'); + + done(); + }).catch(done.fail); + }); + }); + + describe('createTempFile', () => { + beforeEach(() => { + document.body.innerHTML += '<div class="flash-container"></div>'; + }); + + afterEach(() => { + document.querySelector('.flash-container').remove(); + }); + + it('creates temp file', (done) => { + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then((f) => { + expect(f.tempFile).toBeTruthy(); + expect(store.state.tree.length).toBe(1); + + done(); + }).catch(done.fail); + }); + + it('adds tmp file to open files', (done) => { + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then((f) => { + expect(store.state.openFiles.length).toBe(1); + expect(store.state.openFiles[0].name).toBe(f.name); + + done(); + }).catch(done.fail); + }); + + it('sets tmp file as active', (done) => { + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then((f) => { + expect(f.active).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('enters edit mode if file is not base64', (done) => { + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then(() => { + expect(store.state.editMode).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('does not enter edit mode if file is base64', (done) => { + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + base64: true, + }).then(() => { + expect(store.state.editMode).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('creates flash message is file already exists', (done) => { + store.state.tree.push(file('test', '1', 'blob')); + + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then(() => { + expect(document.querySelector('.flash-alert')).not.toBeNull(); + + done(); + }).catch(done.fail); + }); + + it('increases level of file', (done) => { + store.state.level = 1; + + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then((f) => { + expect(f.level).toBe(2); + + done(); + }).catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js new file mode 100644 index 00000000000..393a797c6a3 --- /dev/null +++ b/spec/javascripts/repo/stores/actions/tree_spec.js @@ -0,0 +1,469 @@ +import Vue from 'vue'; +import store from '~/repo/stores'; +import service from '~/repo/services'; +import { file, resetStore } from '../../helpers'; + +describe('Multi-file store tree actions', () => { + afterEach(() => { + resetStore(store); + }); + + describe('getTreeData', () => { + beforeEach(() => { + spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ + headers: { + 'page-title': 'test', + }, + json: () => Promise.resolve({ + last_commit_path: 'last_commit_path', + parent_tree_url: 'parent_tree_url', + path: '/', + trees: [{ name: 'tree' }], + blobs: [{ name: 'blob' }], + submodules: [{ name: 'submodule' }], + }), + })); + spyOn(history, 'pushState'); + + Object.assign(store.state.endpoints, { + rootEndpoint: 'rootEndpoint', + }); + }); + + it('calls service getTreeData', (done) => { + store.dispatch('getTreeData') + .then(() => { + expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint'); + + done(); + }).catch(done.fail); + }); + + it('adds data into tree', (done) => { + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.tree.length).toBe(3); + expect(store.state.tree[0].type).toBe('tree'); + expect(store.state.tree[1].type).toBe('submodule'); + expect(store.state.tree[2].type).toBe('blob'); + + done(); + }).catch(done.fail); + }); + + it('sets parent tree URL', (done) => { + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.parentTreeUrl).toBe('parent_tree_url'); + + done(); + }).catch(done.fail); + }); + + it('sets last commit path', (done) => { + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.lastCommitPath).toBe('last_commit_path'); + + done(); + }).catch(done.fail); + }); + + it('sets root if not currently at root', (done) => { + store.state.isInitialRoot = false; + + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.isInitialRoot).toBeTruthy(); + expect(store.state.isRoot).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('sets page title', (done) => { + store.dispatch('getTreeData') + .then(() => { + expect(document.title).toBe('test'); + + done(); + }).catch(done.fail); + }); + + it('toggles loading', (done) => { + store.dispatch('getTreeData') + .then(() => { + expect(store.state.loading).toBeTruthy(); + + return Vue.nextTick(); + }) + .then(() => { + expect(store.state.loading).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('calls pushState with endpoint', (done) => { + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'rootEndpoint'); + + done(); + }).catch(done.fail); + }); + + it('calls getLastCommitData if prevLastCommitPath is not null', (done) => { + const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData'); + const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line + store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line + store.state.prevLastCommitPath = 'test'; + + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(getLastCommitDataSpy).toHaveBeenCalledWith(store.state); + + store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line + + done(); + }).catch(done.fail); + }); + }); + + describe('toggleTreeOpen', () => { + let oldGetTreeData; + let getTreeDataSpy; + let tree; + + beforeEach(() => { + getTreeDataSpy = jasmine.createSpy('getTreeData'); + + oldGetTreeData = store._actions.getTreeData; // eslint-disable-line + store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line + + tree = { + opened: false, + tree: [], + }; + }); + + afterEach(() => { + store._actions.getTreeData = oldGetTreeData; // eslint-disable-line + }); + + it('toggles the tree open', (done) => { + store.dispatch('toggleTreeOpen', { + endpoint: 'test', + tree, + }).then(() => { + expect(tree.opened).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('calls getTreeData if tree is closed', (done) => { + store.dispatch('toggleTreeOpen', { + endpoint: 'test', + tree, + }).then(() => { + expect(getTreeDataSpy).toHaveBeenCalledWith({ + endpoint: 'test', + tree, + }); + expect(store.state.previousUrl).toBe('test'); + + done(); + }).catch(done.fail); + }); + + it('resets entries tree', (done) => { + Object.assign(tree, { + opened: true, + tree: ['a'], + }); + + store.dispatch('toggleTreeOpen', { + endpoint: 'test', + tree, + }).then(() => { + expect(tree.tree.length).toBe(0); + + done(); + }).catch(done.fail); + }); + + it('pushes new state', (done) => { + spyOn(history, 'pushState'); + Object.assign(tree, { + opened: true, + parentTreeUrl: 'testing', + }); + + store.dispatch('toggleTreeOpen', { + endpoint: 'test', + tree, + }).then(() => { + expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'testing'); + + done(); + }).catch(done.fail); + }); + }); + + describe('clickedTreeRow', () => { + describe('tree', () => { + let toggleTreeOpenSpy; + let oldToggleTreeOpen; + + beforeEach(() => { + toggleTreeOpenSpy = jasmine.createSpy('toggleTreeOpen'); + + oldToggleTreeOpen = store._actions.toggleTreeOpen; // eslint-disable-line + store._actions.toggleTreeOpen = [toggleTreeOpenSpy]; // eslint-disable-line + }); + + afterEach(() => { + store._actions.toggleTreeOpen = oldToggleTreeOpen; // eslint-disable-line + }); + + it('opens tree', (done) => { + const tree = { + url: 'a', + type: 'tree', + }; + + store.dispatch('clickedTreeRow', tree) + .then(() => { + expect(toggleTreeOpenSpy).toHaveBeenCalledWith({ + endpoint: tree.url, + tree, + }); + + done(); + }).catch(done.fail); + }); + }); + + describe('submodule', () => { + let row; + + beforeEach(() => { + spyOn(gl.utils, 'visitUrl'); + + row = { + url: 'submoduleurl', + type: 'submodule', + loading: false, + }; + }); + + it('toggles loading for row', (done) => { + store.dispatch('clickedTreeRow', row) + .then(() => { + expect(row.loading).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('opens submodule URL', (done) => { + store.dispatch('clickedTreeRow', row) + .then(() => { + expect(gl.utils.visitUrl).toHaveBeenCalledWith('submoduleurl'); + + done(); + }).catch(done.fail); + }); + }); + + describe('blob', () => { + let row; + + beforeEach(() => { + row = { + type: 'blob', + opened: false, + }; + }); + + it('calls getFileData', (done) => { + const getFileDataSpy = jasmine.createSpy('getFileData'); + const oldGetFileData = store._actions.getFileData; // eslint-disable-line + store._actions.getFileData = [getFileDataSpy]; // eslint-disable-line + + store.dispatch('clickedTreeRow', row) + .then(() => { + expect(getFileDataSpy).toHaveBeenCalledWith(row); + + store._actions.getFileData = oldGetFileData; // eslint-disable-line + + done(); + }).catch(done.fail); + }); + + it('calls setFileActive when file is opened', (done) => { + const setFileActiveSpy = jasmine.createSpy('setFileActive'); + const oldSetFileActive = store._actions.setFileActive; // eslint-disable-line + store._actions.setFileActive = [setFileActiveSpy]; // eslint-disable-line + + row.opened = true; + + store.dispatch('clickedTreeRow', row) + .then(() => { + expect(setFileActiveSpy).toHaveBeenCalledWith(row); + + store._actions.setFileActive = oldSetFileActive; // eslint-disable-line + + done(); + }).catch(done.fail); + }); + }); + }); + + describe('createTempTree', () => { + it('creates temp tree', (done) => { + store.dispatch('createTempTree', 'test') + .then(() => { + expect(store.state.tree[0].tempFile).toBeTruthy(); + expect(store.state.tree[0].name).toBe('test'); + expect(store.state.tree[0].type).toBe('tree'); + + done(); + }).catch(done.fail); + }); + + it('creates .gitkeep file in temp tree', (done) => { + store.dispatch('createTempTree', 'test') + .then(() => { + expect(store.state.tree[0].tree[0].tempFile).toBeTruthy(); + expect(store.state.tree[0].tree[0].name).toBe('.gitkeep'); + + done(); + }).catch(done.fail); + }); + + it('creates new folder inside another tree', (done) => { + const tree = { + type: 'tree', + name: 'testing', + tree: [], + }; + + store.state.tree.push(tree); + + store.dispatch('createTempTree', 'testing/test') + .then(() => { + expect(store.state.tree[0].name).toBe('testing'); + expect(store.state.tree[0].tree[0].tempFile).toBeTruthy(); + expect(store.state.tree[0].tree[0].name).toBe('test'); + expect(store.state.tree[0].tree[0].type).toBe('tree'); + + done(); + }).catch(done.fail); + }); + + it('does not create new tree if already exists', (done) => { + const tree = { + type: 'tree', + name: 'testing', + tree: [], + }; + + store.state.tree.push(tree); + + store.dispatch('createTempTree', 'testing/test') + .then(() => { + expect(store.state.tree[0].name).toBe('testing'); + expect(store.state.tree[0].tempFile).toBeUndefined(); + + done(); + }).catch(done.fail); + }); + }); + + describe('getLastCommitData', () => { + beforeEach(() => { + spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({ + headers: { + 'more-logs-url': null, + }, + json: () => Promise.resolve([{ + type: 'tree', + file_name: 'testing', + commit: { + message: 'commit message', + authored_date: '123', + }, + }]), + })); + + store.state.tree.push(file('testing', '1', 'tree')); + store.state.lastCommitPath = 'lastcommitpath'; + }); + + it('calls service with lastCommitPath', (done) => { + store.dispatch('getLastCommitData') + .then(() => { + expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath'); + + done(); + }).catch(done.fail); + }); + + it('updates trees last commit data', (done) => { + store.dispatch('getLastCommitData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.tree[0].lastCommit.message).toBe('commit message'); + + done(); + }).catch(done.fail); + }); + + it('does not update entry if not found', (done) => { + store.state.tree[0].name = 'a'; + + store.dispatch('getLastCommitData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.tree[0].lastCommit.message).not.toBe('commit message'); + + done(); + }).catch(done.fail); + }); + }); + + describe('updateDirectoryData', () => { + it('adds data into tree', (done) => { + const tree = { + tree: [], + }; + const data = { + trees: [{ name: 'tree' }], + submodules: [{ name: 'submodule' }], + blobs: [{ name: 'blob' }], + }; + + store.dispatch('updateDirectoryData', { + data, + tree, + }).then(() => { + expect(tree.tree[0].name).toBe('tree'); + expect(tree.tree[0].type).toBe('tree'); + expect(tree.tree[1].name).toBe('submodule'); + expect(tree.tree[1].type).toBe('submodule'); + expect(tree.tree[2].name).toBe('blob'); + expect(tree.tree[2].type).toBe('blob'); + + done(); + }).catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js new file mode 100644 index 00000000000..f2a7a698912 --- /dev/null +++ b/spec/javascripts/repo/stores/actions_spec.js @@ -0,0 +1,419 @@ +import Vue from 'vue'; +import store from '~/repo/stores'; +import service from '~/repo/services'; +import { resetStore, file } from '../helpers'; + +describe('Multi-file store actions', () => { + afterEach(() => { + resetStore(store); + }); + + describe('redirectToUrl', () => { + it('calls visitUrl', (done) => { + spyOn(gl.utils, 'visitUrl'); + + store.dispatch('redirectToUrl', 'test') + .then(() => { + expect(gl.utils.visitUrl).toHaveBeenCalledWith('test'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('setInitialData', () => { + it('commits initial data', (done) => { + store.dispatch('setInitialData', { canCommit: true }) + .then(() => { + expect(store.state.canCommit).toBeTruthy(); + done(); + }) + .catch(done.fail); + }); + }); + + describe('closeDiscardPopup', () => { + it('closes the discard popup', (done) => { + store.dispatch('closeDiscardPopup', false) + .then(() => { + expect(store.state.discardPopupOpen).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('discardAllChanges', () => { + beforeEach(() => { + store.state.openFiles.push(file()); + store.state.openFiles[0].changed = true; + }); + }); + + describe('closeAllFiles', () => { + beforeEach(() => { + store.state.openFiles.push(file()); + store.state.openFiles[0].opened = true; + }); + + it('closes all open files', (done) => { + store.dispatch('closeAllFiles') + .then(() => { + expect(store.state.openFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('toggleEditMode', () => { + it('toggles edit mode', (done) => { + store.state.editMode = true; + + store.dispatch('toggleEditMode') + .then(() => { + expect(store.state.editMode).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('sets preview mode', (done) => { + store.state.currentBlobView = 'repo-editor'; + store.state.editMode = true; + + store.dispatch('toggleEditMode') + .then(Vue.nextTick) + .then(() => { + expect(store.state.currentBlobView).toBe('repo-preview'); + + done(); + }).catch(done.fail); + }); + + it('opens discard popup if there are changed files', (done) => { + store.state.editMode = true; + store.state.openFiles.push(file()); + store.state.openFiles[0].changed = true; + + store.dispatch('toggleEditMode') + .then(() => { + expect(store.state.discardPopupOpen).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('can force closed if there are changed files', (done) => { + store.state.editMode = true; + store.state.openFiles.push(file()); + store.state.openFiles[0].changed = true; + + store.dispatch('toggleEditMode', true) + .then(() => { + expect(store.state.discardPopupOpen).toBeFalsy(); + expect(store.state.editMode).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('discards file changes', (done) => { + const f = file(); + store.state.editMode = true; + store.state.tree.push(f); + store.state.openFiles.push(f); + f.changed = true; + + store.dispatch('toggleEditMode', true) + .then(Vue.nextTick) + .then(() => { + expect(f.changed).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + }); + + describe('toggleBlobView', () => { + it('sets edit mode view if in edit mode', (done) => { + store.state.editMode = true; + + store.dispatch('toggleBlobView') + .then(() => { + expect(store.state.currentBlobView).toBe('repo-editor'); + + done(); + }) + .catch(done.fail); + }); + + it('sets preview mode view if not in edit mode', (done) => { + store.dispatch('toggleBlobView') + .then(() => { + expect(store.state.currentBlobView).toBe('repo-preview'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('checkCommitStatus', () => { + beforeEach(() => { + store.state.project.id = 2; + store.state.currentBranch = 'master'; + store.state.currentRef = '1'; + }); + + it('calls service', (done) => { + spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ + commit: { id: '123' }, + })); + + store.dispatch('checkCommitStatus') + .then(() => { + expect(service.getBranchData).toHaveBeenCalledWith(2, 'master'); + + done(); + }) + .catch(done.fail); + }); + + it('returns true if current ref does not equal returned ID', (done) => { + spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ + commit: { id: '123' }, + })); + + store.dispatch('checkCommitStatus') + .then((val) => { + expect(val).toBeTruthy(); + + done(); + }) + .catch(done.fail); + }); + + it('returns false if current ref equals returned ID', (done) => { + spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ + commit: { id: '1' }, + })); + + store.dispatch('checkCommitStatus') + .then((val) => { + expect(val).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('commitChanges', () => { + let payload; + + beforeEach(() => { + spyOn(window, 'scrollTo'); + + document.body.innerHTML += '<div class="flash-container"></div>'; + + store.state.project.id = 123; + payload = { + branch: 'master', + }; + }); + + afterEach(() => { + document.querySelector('.flash-container').remove(); + }); + + describe('success', () => { + beforeEach(() => { + spyOn(service, 'commit').and.returnValue(Promise.resolve({ + id: '123456', + short_id: '123', + message: 'test message', + committed_date: 'date', + stats: { + additions: '1', + deletions: '2', + }, + })); + }); + + it('calls service', (done) => { + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + expect(service.commit).toHaveBeenCalledWith(123, payload); + + done(); + }).catch(done.fail); + }); + + it('shows flash notice', (done) => { + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + const alert = document.querySelector('.flash-container'); + + expect(alert.querySelector('.flash-notice')).not.toBeNull(); + expect(alert.textContent.trim()).toBe( + 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.', + ); + + done(); + }).catch(done.fail); + }); + + it('adds commit data to changed files', (done) => { + const changedFile = file(); + const f = file(); + changedFile.changed = true; + + store.state.openFiles.push(changedFile, f); + + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + expect(changedFile.lastCommit.message).toBe('test message'); + expect(f.lastCommit.message).not.toBe('test message'); + + done(); + }).catch(done.fail); + }); + + it('toggles edit mode', (done) => { + store.state.editMode = true; + + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + expect(store.state.editMode).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('closes all files', (done) => { + store.state.openFiles.push(file()); + store.state.openFiles[0].opened = true; + + store.dispatch('commitChanges', { payload, newMr: false }) + .then(Vue.nextTick) + .then(() => { + expect(store.state.openFiles.length).toBe(0); + + done(); + }).catch(done.fail); + }); + + it('scrolls to top of page', (done) => { + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + expect(window.scrollTo).toHaveBeenCalledWith(0, 0); + + done(); + }).catch(done.fail); + }); + + it('updates commit ref', (done) => { + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + expect(store.state.currentRef).toBe('123456'); + + done(); + }).catch(done.fail); + }); + + it('redirects to new merge request page', (done) => { + spyOn(gl.utils, 'visitUrl'); + + store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch='; + + store.dispatch('commitChanges', { payload, newMr: true }) + .then(() => { + expect(gl.utils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master'); + + done(); + }).catch(done.fail); + }); + }); + + describe('failed', () => { + beforeEach(() => { + spyOn(service, 'commit').and.returnValue(Promise.resolve({ + message: 'failed message', + })); + }); + + it('shows failed message', (done) => { + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + const alert = document.querySelector('.flash-container'); + + expect(alert.textContent.trim()).toBe( + 'failed message', + ); + + done(); + }).catch(done.fail); + }); + }); + }); + + describe('createTempEntry', () => { + it('creates a temp tree', (done) => { + store.dispatch('createTempEntry', { + name: 'test', + type: 'tree', + }) + .then(() => { + expect(store.state.tree.length).toBe(1); + expect(store.state.tree[0].tempFile).toBeTruthy(); + expect(store.state.tree[0].type).toBe('tree'); + + done(); + }) + .catch(done.fail); + }); + + it('creates temp file', (done) => { + store.dispatch('createTempEntry', { + name: 'test', + type: 'blob', + }) + .then(() => { + expect(store.state.tree.length).toBe(1); + expect(store.state.tree[0].tempFile).toBeTruthy(); + expect(store.state.tree[0].type).toBe('blob'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('popHistoryState', () => { + + }); + + describe('scrollToTab', () => { + it('focuses the current active element', (done) => { + document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>'; + const el = document.querySelector('.repo-tab'); + spyOn(el, 'focus'); + + store.dispatch('scrollToTab') + .then(() => { + setTimeout(() => { + expect(el.focus).toHaveBeenCalled(); + + document.getElementById('tabs').remove(); + + done(); + }); + }) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js new file mode 100644 index 00000000000..952b8ec3a59 --- /dev/null +++ b/spec/javascripts/repo/stores/getters_spec.js @@ -0,0 +1,146 @@ +import * as getters from '~/repo/stores/getters'; +import state from '~/repo/stores/state'; +import { file } from '../helpers'; + +describe('Multi-file store getters', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('treeList', () => { + it('returns flat tree list', () => { + localState.tree.push(file('1')); + localState.tree[0].tree.push(file('2')); + localState.tree[0].tree[0].tree.push(file('3')); + + const treeList = getters.treeList(localState); + + expect(treeList.length).toBe(3); + expect(treeList[1].name).toBe(localState.tree[0].tree[0].name); + expect(treeList[2].name).toBe(localState.tree[0].tree[0].tree[0].name); + }); + }); + + describe('changedFiles', () => { + it('returns a list of changed opened files', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('changed')); + localState.openFiles[1].changed = true; + + const changedFiles = getters.changedFiles(localState); + + expect(changedFiles.length).toBe(1); + expect(changedFiles[0].name).toBe('changed'); + }); + }); + + describe('activeFile', () => { + it('returns the current active file', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('active')); + localState.openFiles[1].active = true; + + expect(getters.activeFile(localState).name).toBe('active'); + }); + + it('returns undefined if no active files are found', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('active')); + + expect(getters.activeFile(localState)).toBeUndefined(); + }); + }); + + describe('activeFileExtension', () => { + it('returns the file extension for the current active file', () => { + localState.openFiles.push(file('active')); + localState.openFiles[0].active = true; + localState.openFiles[0].path = 'test.js'; + + expect(getters.activeFileExtension(localState)).toBe('.js'); + + localState.openFiles[0].path = 'test.es6.js'; + + expect(getters.activeFileExtension(localState)).toBe('.js'); + }); + }); + + describe('isCollapsed', () => { + it('returns true if state has open files', () => { + localState.openFiles.push(file()); + + expect(getters.isCollapsed(localState)).toBeTruthy(); + }); + + it('returns false if state has no open files', () => { + expect(getters.isCollapsed(localState)).toBeFalsy(); + }); + }); + + describe('canEditFile', () => { + beforeEach(() => { + localState.onTopOfBranch = true; + localState.canCommit = true; + + localState.openFiles.push(file()); + localState.openFiles[0].active = true; + }); + + it('returns true if user can commit and has open files', () => { + expect(getters.canEditFile(localState)).toBeTruthy(); + }); + + it('returns false if user can commit and has no open files', () => { + localState.openFiles = []; + + expect(getters.canEditFile(localState)).toBeFalsy(); + }); + + it('returns false if user can commit and active file is binary', () => { + localState.openFiles[0].binary = true; + + expect(getters.canEditFile(localState)).toBeFalsy(); + }); + + it('returns false if user cant commit', () => { + localState.canCommit = false; + + expect(getters.canEditFile(localState)).toBeFalsy(); + }); + + it('returns false if user can commit but on a branch', () => { + localState.onTopOfBranch = false; + + expect(getters.canEditFile(localState)).toBeFalsy(); + }); + }); + + describe('modifiedFiles', () => { + it('returns a list of modified files', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('changed')); + localState.openFiles[1].changed = true; + + const modifiedFiles = getters.modifiedFiles(localState); + + expect(modifiedFiles.length).toBe(1); + expect(modifiedFiles[0].name).toBe('changed'); + }); + }); + + describe('addedFiles', () => { + it('returns a list of added files', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('added')); + localState.openFiles[1].changed = true; + localState.openFiles[1].tempFile = true; + + const modifiedFiles = getters.addedFiles(localState); + + expect(modifiedFiles.length).toBe(1); + expect(modifiedFiles[0].name).toBe('added'); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js new file mode 100644 index 00000000000..3c06794d5e3 --- /dev/null +++ b/spec/javascripts/repo/stores/mutations/branch_spec.js @@ -0,0 +1,18 @@ +import mutations from '~/repo/stores/mutations/branch'; +import state from '~/repo/stores/state'; + +describe('Multi-file store branch mutations', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('SET_CURRENT_BRANCH', () => { + it('sets currentBranch', () => { + mutations.SET_CURRENT_BRANCH(localState, 'master'); + + expect(localState.currentBranch).toBe('master'); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js new file mode 100644 index 00000000000..2f2835dde1f --- /dev/null +++ b/spec/javascripts/repo/stores/mutations/file_spec.js @@ -0,0 +1,131 @@ +import mutations from '~/repo/stores/mutations/file'; +import state from '~/repo/stores/state'; +import { file } from '../../helpers'; + +describe('Multi-file store file mutations', () => { + let localState; + let localFile; + + beforeEach(() => { + localState = state(); + localFile = file(); + }); + + describe('SET_FILE_ACTIVE', () => { + it('sets the file active', () => { + mutations.SET_FILE_ACTIVE(localState, { + file: localFile, + active: true, + }); + + expect(localFile.active).toBeTruthy(); + }); + }); + + describe('TOGGLE_FILE_OPEN', () => { + beforeEach(() => { + mutations.TOGGLE_FILE_OPEN(localState, localFile); + }); + + it('adds into opened files', () => { + expect(localFile.opened).toBeTruthy(); + expect(localState.openFiles.length).toBe(1); + }); + + it('removes from opened files', () => { + mutations.TOGGLE_FILE_OPEN(localState, localFile); + + expect(localFile.opened).toBeFalsy(); + expect(localState.openFiles.length).toBe(0); + }); + }); + + describe('SET_FILE_DATA', () => { + it('sets extra file data', () => { + mutations.SET_FILE_DATA(localState, { + data: { + blame_path: 'blame', + commits_path: 'commits', + permalink: 'permalink', + raw_path: 'raw', + binary: true, + html: 'html', + render_error: 'render_error', + }, + file: localFile, + }); + + expect(localFile.blamePath).toBe('blame'); + expect(localFile.commitsPath).toBe('commits'); + expect(localFile.permalink).toBe('permalink'); + expect(localFile.rawPath).toBe('raw'); + expect(localFile.binary).toBeTruthy(); + expect(localFile.html).toBe('html'); + expect(localFile.renderError).toBe('render_error'); + }); + }); + + describe('SET_FILE_RAW_DATA', () => { + it('sets raw data', () => { + mutations.SET_FILE_RAW_DATA(localState, { + file: localFile, + raw: 'testing', + }); + + expect(localFile.raw).toBe('testing'); + }); + }); + + describe('UPDATE_FILE_CONTENT', () => { + beforeEach(() => { + localFile.raw = 'test'; + }); + + it('sets content', () => { + mutations.UPDATE_FILE_CONTENT(localState, { + file: localFile, + content: 'test', + }); + + expect(localFile.content).toBe('test'); + }); + + it('sets changed if content does not match raw', () => { + mutations.UPDATE_FILE_CONTENT(localState, { + file: localFile, + content: 'testing', + }); + + expect(localFile.content).toBe('testing'); + expect(localFile.changed).toBeTruthy(); + }); + }); + + describe('DISCARD_FILE_CHANGES', () => { + beforeEach(() => { + localFile.content = 'test'; + localFile.changed = true; + }); + + it('resets content and changed', () => { + mutations.DISCARD_FILE_CHANGES(localState, localFile); + + expect(localFile.content).toBe(''); + expect(localFile.changed).toBeFalsy(); + }); + }); + + describe('CREATE_TMP_FILE', () => { + it('adds file into parent tree', () => { + const f = file(); + + mutations.CREATE_TMP_FILE(localState, { + file: f, + parent: localFile, + }); + + expect(localFile.tree.length).toBe(1); + expect(localFile.tree[0].name).toBe(f.name); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js new file mode 100644 index 00000000000..1c76cfed9c8 --- /dev/null +++ b/spec/javascripts/repo/stores/mutations/tree_spec.js @@ -0,0 +1,71 @@ +import mutations from '~/repo/stores/mutations/tree'; +import state from '~/repo/stores/state'; +import { file } from '../../helpers'; + +describe('Multi-file store tree mutations', () => { + let localState; + let localTree; + + beforeEach(() => { + localState = state(); + localTree = file(); + }); + + describe('TOGGLE_TREE_OPEN', () => { + it('toggles tree open', () => { + mutations.TOGGLE_TREE_OPEN(localState, localTree); + + expect(localTree.opened).toBeTruthy(); + + mutations.TOGGLE_TREE_OPEN(localState, localTree); + + expect(localTree.opened).toBeFalsy(); + }); + }); + + describe('SET_DIRECTORY_DATA', () => { + const data = [{ + name: 'tree', + }, + { + name: 'submodule', + }, + { + name: 'blob', + }]; + + it('adds directory data', () => { + mutations.SET_DIRECTORY_DATA(localState, { + data, + tree: localState, + }); + + expect(localState.tree.length).toBe(3); + expect(localState.tree[0].name).toBe('tree'); + expect(localState.tree[1].name).toBe('submodule'); + expect(localState.tree[2].name).toBe('blob'); + }); + }); + + describe('SET_PARENT_TREE_URL', () => { + it('sets the parent tree url', () => { + mutations.SET_PARENT_TREE_URL(localState, 'test'); + + expect(localState.parentTreeUrl).toBe('test'); + }); + }); + + describe('CREATE_TMP_TREE', () => { + it('adds tree into parent tree', () => { + const tmpEntry = file(); + + mutations.CREATE_TMP_TREE(localState, { + tmpEntry, + parent: localTree, + }); + + expect(localTree.tree.length).toBe(1); + expect(localTree.tree[0].name).toBe(tmpEntry.name); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js new file mode 100644 index 00000000000..d1c9885e01d --- /dev/null +++ b/spec/javascripts/repo/stores/mutations_spec.js @@ -0,0 +1,117 @@ +import mutations from '~/repo/stores/mutations'; +import state from '~/repo/stores/state'; +import { file } from '../helpers'; + +describe('Multi-file store mutations', () => { + let localState; + let entry; + + beforeEach(() => { + localState = state(); + entry = file(); + }); + + describe('SET_INITIAL_DATA', () => { + it('sets all initial data', () => { + mutations.SET_INITIAL_DATA(localState, { + test: 'test', + }); + + expect(localState.test).toBe('test'); + }); + }); + + describe('SET_PREVIEW_MODE', () => { + it('sets currentBlobView to repo-preview', () => { + mutations.SET_PREVIEW_MODE(localState); + + expect(localState.currentBlobView).toBe('repo-preview'); + + localState.currentBlobView = 'testing'; + + mutations.SET_PREVIEW_MODE(localState); + + expect(localState.currentBlobView).toBe('repo-preview'); + }); + }); + + describe('SET_EDIT_MODE', () => { + it('sets currentBlobView to repo-editor', () => { + mutations.SET_EDIT_MODE(localState); + + expect(localState.currentBlobView).toBe('repo-editor'); + + localState.currentBlobView = 'testing'; + + mutations.SET_EDIT_MODE(localState); + + expect(localState.currentBlobView).toBe('repo-editor'); + }); + }); + + describe('TOGGLE_LOADING', () => { + it('toggles loading of entry', () => { + mutations.TOGGLE_LOADING(localState, entry); + + expect(entry.loading).toBeTruthy(); + + mutations.TOGGLE_LOADING(localState, entry); + + expect(entry.loading).toBeFalsy(); + }); + }); + + describe('TOGGLE_EDIT_MODE', () => { + it('toggles editMode', () => { + mutations.TOGGLE_EDIT_MODE(localState); + + expect(localState.editMode).toBeTruthy(); + + mutations.TOGGLE_EDIT_MODE(localState); + + expect(localState.editMode).toBeFalsy(); + }); + }); + + describe('TOGGLE_DISCARD_POPUP', () => { + it('sets discardPopupOpen', () => { + mutations.TOGGLE_DISCARD_POPUP(localState, true); + + expect(localState.discardPopupOpen).toBeTruthy(); + + mutations.TOGGLE_DISCARD_POPUP(localState, false); + + expect(localState.discardPopupOpen).toBeFalsy(); + }); + }); + + describe('SET_COMMIT_REF', () => { + it('sets currentRef', () => { + mutations.SET_COMMIT_REF(localState, '123'); + + expect(localState.currentRef).toBe('123'); + }); + }); + + describe('SET_ROOT', () => { + it('sets isRoot & initialRoot', () => { + mutations.SET_ROOT(localState, true); + + expect(localState.isRoot).toBeTruthy(); + expect(localState.isInitialRoot).toBeTruthy(); + + mutations.SET_ROOT(localState, false); + + expect(localState.isRoot).toBeFalsy(); + expect(localState.isInitialRoot).toBeFalsy(); + }); + }); + + describe('SET_PREVIOUS_URL', () => { + it('sets previousUrl', () => { + mutations.SET_PREVIOUS_URL(localState, 'testing'); + + expect(localState.previousUrl).toBe('testing'); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js new file mode 100644 index 00000000000..37287c587d7 --- /dev/null +++ b/spec/javascripts/repo/stores/utils_spec.js @@ -0,0 +1,102 @@ +import * as utils from '~/repo/stores/utils'; + +describe('Multi-file store utils', () => { + describe('setPageTitle', () => { + it('sets the document page title', () => { + utils.setPageTitle('test'); + + expect(document.title).toBe('test'); + }); + }); + + describe('pushState', () => { + it('calls history.pushState', () => { + spyOn(history, 'pushState'); + + utils.pushState('test'); + + expect(history.pushState).toHaveBeenCalledWith({ url: 'test' }, '', 'test'); + }); + }); + + describe('createTemp', () => { + it('creates temp tree', () => { + const tmp = utils.createTemp({ + name: 'test', + path: 'test', + type: 'tree', + level: 0, + changed: false, + content: '', + base64: '', + }); + + expect(tmp.tempFile).toBeTruthy(); + expect(tmp.icon).toBe('fa-folder'); + }); + + it('creates temp file', () => { + const tmp = utils.createTemp({ + name: 'test', + path: 'test', + type: 'blob', + level: 0, + changed: false, + content: '', + base64: '', + }); + + expect(tmp.tempFile).toBeTruthy(); + expect(tmp.icon).toBe('fa-file-text-o'); + }); + }); + + describe('findIndexOfFile', () => { + let state; + + beforeEach(() => { + state = [{ + path: '1', + }, { + path: '2', + }]; + }); + + it('finds in the index of an entry by path', () => { + const index = utils.findIndexOfFile(state, { + path: '2', + }); + + expect(index).toBe(1); + }); + }); + + describe('findEntry', () => { + let state; + + beforeEach(() => { + state = { + tree: [{ + type: 'tree', + name: 'test', + }, { + type: 'blob', + name: 'file', + }], + }; + }); + + it('returns an entry found by name', () => { + const foundEntry = utils.findEntry(state, 'tree', 'test'); + + expect(foundEntry.type).toBe('tree'); + expect(foundEntry.name).toBe('test'); + }); + + it('returns undefined when no entry found', () => { + const foundEntry = utils.findEntry(state, 'blob', 'test'); + + expect(foundEntry).toBeUndefined(); + }); + }); +}); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index a2394857b82..fdfc59a6f12 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -191,8 +191,6 @@ import '~/lib/utils/common_utils'; // browsers will not trigger default behavior (form submit, in this // example) on JavaScript-created keypresses. expect(submitSpy).not.toHaveBeenTriggered(); - // Does a worse job at capturing the intent of the test, but works. - expect(enterKeyEvent.isDefaultPrevented()).toBe(true); }); }); }).call(window); diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index 0682b463043..3b094d20838 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -1,6 +1,6 @@ /* eslint-disable quote-props*/ -const sidebarMockData = { +const RESPONSE_MAP = { 'GET': { '/gitlab-org/gitlab-shell/issues/5.json': { id: 45, @@ -66,6 +66,65 @@ const sidebarMockData = { }, labels: [], }, + '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar': { + assignees: [ + { + name: 'User 0', + username: 'user0', + id: 22, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/user0', + }, + { + name: 'Marguerite Bartell', + username: 'tajuana', + id: 18, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/tajuana', + }, + { + name: 'Laureen Ritchie', + username: 'michaele.will', + id: 16, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/michaele.will', + }, + ], + human_time_estimate: null, + human_total_time_spent: null, + participants: [ + { + name: 'User 0', + username: 'user0', + id: 22, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/user0', + }, + { + name: 'Marguerite Bartell', + username: 'tajuana', + id: 18, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/tajuana', + }, + { + name: 'Laureen Ritchie', + username: 'michaele.will', + id: 16, + state: 'active', + avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http: //localhost:3001/michaele.will', + }, + ], + subscribed: true, + time_estimate: 0, + total_time_spent: 0, + }, '/autocomplete/projects?project_id=15': [ { 'id': 0, @@ -113,9 +172,10 @@ const sidebarMockData = { }, }; -export default { +const mockData = { + responseMap: RESPONSE_MAP, mediator: { - endpoint: '/gitlab-org/gitlab-shell/issues/5.json', + endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar', toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription', moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', @@ -141,12 +201,14 @@ export default { name: 'Administrator', username: 'root', }, +}; - sidebarMockInterceptor(request, next) { - const body = sidebarMockData[request.method.toUpperCase()][request.url]; +mockData.sidebarMockInterceptor = function (request, next) { + const body = this.responseMap[request.method.toUpperCase()][request.url]; - next(request.respondWith(JSON.stringify(body), { - status: 200, - })); - }, -}; + next(request.respondWith(JSON.stringify(body), { + status: 200, + })); +}.bind(mockData); + +export default mockData; diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index 7deb1fd2118..14c34d5a78c 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -33,10 +33,29 @@ describe('Sidebar mediator', () => { .catch(done.fail); }); - it('fetches the data', () => { - spyOn(this.mediator.service, 'get').and.callThrough(); - this.mediator.fetch(); - expect(this.mediator.service.get).toHaveBeenCalled(); + it('fetches the data', (done) => { + const mockData = Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar']; + spyOn(this.mediator, 'processFetchedData').and.callThrough(); + + this.mediator.fetch() + .then(() => { + expect(this.mediator.processFetchedData).toHaveBeenCalledWith(mockData); + }) + .then(done) + .catch(done.fail); + }); + + it('processes fetched data', () => { + const mockData = Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar']; + this.mediator.processFetchedData(mockData); + + expect(this.mediator.store.assignees).toEqual(mockData.assignees); + expect(this.mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate); + expect(this.mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent); + expect(this.mediator.store.participants).toEqual(mockData.participants); + expect(this.mediator.store.subscribed).toEqual(mockData.subscribed); + expect(this.mediator.store.timeEstimate).toEqual(mockData.time_estimate); + expect(this.mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent); }); it('sets moveToProjectId', () => { diff --git a/spec/javascripts/sidebar/sidebar_service_spec.js b/spec/javascripts/sidebar/sidebar_service_spec.js deleted file mode 100644 index 7324d34d84a..00000000000 --- a/spec/javascripts/sidebar/sidebar_service_spec.js +++ /dev/null @@ -1,66 +0,0 @@ -import Vue from 'vue'; -import SidebarService from '~/sidebar/services/sidebar_service'; -import Mock from './mock_data'; - -describe('Sidebar service', () => { - beforeEach(() => { - Vue.http.interceptors.push(Mock.sidebarMockInterceptor); - this.service = new SidebarService({ - endpoint: '/gitlab-org/gitlab-shell/issues/5.json', - toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription', - moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', - projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', - }); - }); - - afterEach(() => { - SidebarService.singleton = null; - Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor); - }); - - it('gets the data', (done) => { - this.service.get() - .then((resp) => { - expect(resp).toBeDefined(); - done(); - }) - .then(done) - .catch(done.fail); - }); - - it('updates the data', (done) => { - this.service.update('issue[assignee_ids]', [1]) - .then((resp) => { - expect(resp).toBeDefined(); - }) - .then(done) - .catch(done.fail); - }); - - it('gets projects for autocomplete', (done) => { - this.service.getProjectsAutocomplete() - .then((resp) => { - expect(resp).toBeDefined(); - }) - .then(done) - .catch(done.fail); - }); - - it('moves the issue to another project', (done) => { - this.service.moveIssue(123) - .then((resp) => { - expect(resp).toBeDefined(); - }) - .then(done) - .catch(done.fail); - }); - - it('toggles the subscription', (done) => { - this.service.toggleSubscription() - .then((resp) => { - expect(resp).toBeDefined(); - }) - .then(done) - .catch(done.fail); - }); -}); diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js index 51dee64fb93..ea4eae1e23f 100644 --- a/spec/javascripts/sidebar/sidebar_store_spec.js +++ b/spec/javascripts/sidebar/sidebar_store_spec.js @@ -120,6 +120,12 @@ describe('Sidebar store', () => { expect(this.store.isFetching.participants).toEqual(false); }); + it('sets loading state', () => { + this.store.setLoadingState('assignees', true); + + expect(this.store.isLoading.assignees).toEqual(true); + }); + it('set time tracking data', () => { this.store.setTimeTrackingData(Mock.time); expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate); 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 33ed0cb4342..d7af956c9c1 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,140 +1,147 @@ import Vue from 'vue'; -import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline'; +import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; import mockData from '../mock_data'; -const createComponent = (mr) => { - const Component = Vue.extend(pipelineComponent); - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; - describe('MRWidgetPipeline', () => { - describe('props', () => { - it('should have props', () => { - const { mr } = pipelineComponent.props; + let vm; + let Component; - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - }); + beforeEach(() => { + Component = Vue.extend(pipelineComponent); }); - describe('components', () => { - it('should have components added', () => { - expect(pipelineComponent.components['pipeline-stage']).toBeDefined(); - expect(pipelineComponent.components.ciIcon).toBeDefined(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { describe('hasPipeline', () => { it('should return true when there is a pipeline', () => { - expect(Object.keys(mockData.pipeline).length).toBeGreaterThan(0); - - const vm = createComponent({ + vm = mountComponent(Component, { pipeline: mockData.pipeline, + ciStatus: 'success', + hasCi: true, }); - expect(vm.hasPipeline).toBeTruthy(); + expect(vm.hasPipeline).toEqual(true); }); it('should return false when there is no pipeline', () => { - const vm = createComponent({ - pipeline: null, + vm = mountComponent(Component, { + pipeline: {}, }); - expect(vm.hasPipeline).toBeFalsy(); + expect(vm.hasPipeline).toEqual(false); }); }); describe('hasCIError', () => { it('should return false when there is no CI error', () => { - const vm = createComponent({ + vm = mountComponent(Component, { pipeline: mockData.pipeline, - hasCI: true, + hasCi: true, ciStatus: 'success', }); - expect(vm.hasCIError).toBeFalsy(); + expect(vm.hasCIError).toEqual(false); }); it('should return true when there is a CI error', () => { - const vm = createComponent({ + vm = mountComponent(Component, { pipeline: mockData.pipeline, - hasCI: true, + hasCi: true, ciStatus: null, }); - expect(vm.hasCIError).toBeTruthy(); + expect(vm.hasCIError).toEqual(true); }); }); }); - describe('template', () => { - let vm; - let el; - const { pipeline } = mockData; - const mr = { - hasCI: true, - ciStatus: 'success', - pipelineDetailedStatus: pipeline.details.status, - pipeline, - }; - - beforeEach(() => { - vm = createComponent(mr); - el = vm.$el; - }); + describe('rendered output', () => { + it('should render CI error', () => { + vm = mountComponent(Component, { + pipeline: mockData.pipeline, + hasCi: true, + ciStatus: null, + }); - it('should render template elements correctly', () => { - expect(el.classList.contains('mr-widget-heading')).toBeTruthy(); - expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1); - expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipeline.id}`); - expect(el.innerText).toContain('passed'); - expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipeline.path); - expect(el.querySelectorAll('.stage-container').length).toEqual(2); - expect(el.querySelector('.js-ci-error')).toEqual(null); - expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipeline.commit.commit_path); - expect(el.querySelector('.js-commit-link').textContent).toContain(pipeline.commit.short_id); - expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%`); + expect( + vm.$el.querySelector('.media-body').textContent.trim(), + ).toEqual('Could not connect to the CI server. Please check your settings and try again'); }); - it('should list single stage', (done) => { - pipeline.details.stages.splice(0, 1); + describe('with a pipeline', () => { + beforeEach(() => { + vm = mountComponent(Component, { + pipeline: mockData.pipeline, + hasCi: true, + ciStatus: 'success', + }); + }); + + it('should render pipeline ID', () => { + expect( + vm.$el.querySelector('.pipeline-id').textContent.trim(), + ).toEqual(`#${mockData.pipeline.id}`); + }); + + it('should render pipeline status and commit id', () => { + expect( + vm.$el.querySelector('.media-body').textContent.trim(), + ).toContain(mockData.pipeline.details.status.label); - Vue.nextTick(() => { - expect(el.querySelectorAll('.stage-container button').length).toEqual(1); - done(); + expect( + vm.$el.querySelector('.js-commit-link').textContent.trim(), + ).toEqual(mockData.pipeline.commit.short_id); + + expect( + vm.$el.querySelector('.js-commit-link').getAttribute('href'), + ).toEqual(mockData.pipeline.commit.commit_path); }); - }); - it('should not have stages when there is no stage', (done) => { - vm.mr.pipeline.details.stages = []; + it('should render pipeline graph', () => { + expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); + expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length); + }); - Vue.nextTick(() => { - expect(el.querySelectorAll('.stage-container button').length).toEqual(0); - done(); + it('should render coverage information', () => { + expect( + vm.$el.querySelector('.media-body').textContent, + ).toContain(`Coverage ${mockData.pipeline.coverage}`); }); }); - it('should not have coverage text when pipeline has no coverage info', (done) => { - vm.mr.pipeline.coverage = null; + describe('without coverage', () => { + it('should not render a coverage', () => { + const mockCopy = Object.assign({}, mockData); + delete mockCopy.pipeline.coverage; - Vue.nextTick(() => { - expect(el.querySelector('.js-mr-coverage')).toEqual(null); - done(); + vm = mountComponent(Component, { + pipeline: mockCopy.pipeline, + hasCi: true, + ciStatus: 'success', + }); + + expect( + vm.$el.querySelector('.media-body').textContent, + ).not.toContain('Coverage'); }); }); - it('should show CI error when there is a CI error', (done) => { - vm.mr.ciStatus = null; + describe('without a pipeline graph', () => { + it('should not render a pipeline graph', () => { + const mockCopy = Object.assign({}, mockData); + delete mockCopy.pipeline.details.stages; + + vm = mountComponent(Component, { + pipeline: mockCopy.pipeline, + hasCi: true, + ciStatus: 'success', + }); - Vue.nextTick(() => { - expect(el.querySelectorAll('.js-ci-error').length).toEqual(1); - expect(el.innerText).toContain('Could not connect to the CI server'); - expect(el.querySelector('.ci-status-icon svg use').getAttribute('xlink:href')).toContain('status_failed'); - done(); + expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 0795d0aaa82..1ad7c2d8efa 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -202,7 +202,6 @@ export default { "revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1", "email_patches_path": "/root/acets-app/merge_requests/22.patch", "plain_diff_path": "/root/acets-app/merge_requests/22.diff", - "ci_status_path": "/root/acets-app/merge_requests/22/ci_status", "status_path": "/root/acets-app/merge_requests/22.json", "merge_check_path": "/root/acets-app/merge_requests/22/merge_check", "ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status", 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 8832dd161c7..9e6d0aa472c 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -3,13 +3,7 @@ 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 = () => { - delete mrWidgetOptions.el; // Prevent component mounting - gl.mrWidgetData = mockData; - const Component = Vue.extend(mrWidgetOptions); - return new Component(); -}; +import mountComponent from '../helpers/vue_mount_component_helper'; const returnPromise = data => new Promise((resolve) => { resolve({ @@ -22,9 +16,16 @@ const returnPromise = data => new Promise((resolve) => { describe('mrWidgetOptions', () => { let vm; + let MrWidgetOptions; beforeEach(() => { - vm = createComponent(); + // Prevent component mounting + delete mrWidgetOptions.el; + + MrWidgetOptions = Vue.extend(mrWidgetOptions); + vm = mountComponent(MrWidgetOptions, { + mrData: { ...mockData }, + }); }); describe('data', () => { @@ -77,7 +78,7 @@ describe('mrWidgetOptions', () => { }); it('should return true if there is relatedLinks in MR', () => { - vm.mr.relatedLinks = {}; + Vue.set(vm.mr, 'relatedLinks', {}); expect(vm.shouldRenderRelatedLinks).toBeTruthy(); }); }); diff --git a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js deleted file mode 100644 index e667b4b3677..00000000000 --- a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js +++ /dev/null @@ -1,47 +0,0 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; -import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; - -Vue.use(VueResource); - -describe('MRWidgetService', () => { - const mr = { - mergePath: './', - mergeCheckPath: './', - cancelAutoMergePath: './', - removeWIPPath: './', - sourceBranchPath: './', - ciEnvironmentsStatusPath: './', - statusPath: './', - mergeActionsContentPath: './', - isServiceStore: true, - }; - - it('should have store and resources created in constructor', () => { - const service = new MRWidgetService(mr); - - expect(service.mergeResource).toBeDefined(); - expect(service.mergeCheckResource).toBeDefined(); - expect(service.cancelAutoMergeResource).toBeDefined(); - expect(service.removeWIPResource).toBeDefined(); - expect(service.removeSourceBranchResource).toBeDefined(); - expect(service.deploymentsResource).toBeDefined(); - expect(service.pollResource).toBeDefined(); - expect(service.mergeActionsContentResource).toBeDefined(); - }); - - it('should have methods defined', () => { - window.history.pushState({}, null, '/'); - const service = new MRWidgetService(mr); - - expect(service.merge()).toBeDefined(); - expect(service.cancelAutomaticMerge()).toBeDefined(); - expect(service.removeWIP()).toBeDefined(); - expect(service.removeSourceBranch()).toBeDefined(); - expect(service.fetchDeployments()).toBeDefined(); - expect(service.poll()).toBeDefined(); - expect(service.checkStatus()).toBeDefined(); - expect(service.fetchMergeActionsContent()).toBeDefined(); - expect(MRWidgetService.stopEnvironment()).toBeDefined(); - }); -}); diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js index 104da4473ce..a22b6bd3a67 100644 --- a/spec/javascripts/vue_shared/components/icon_spec.js +++ b/spec/javascripts/vue_shared/components/icon_spec.js @@ -11,7 +11,7 @@ describe('Sprite Icon Component', function () { icon = mountComponent(IconComponent, { name: 'test', - size: 99, + size: 32, cssClasses: 'extraclasses', }); }); @@ -34,12 +34,18 @@ describe('Sprite Icon Component', function () { }); it('should properly compute iconSizeClass', function () { - expect(icon.iconSizeClass).toBe('s99'); + expect(icon.iconSizeClass).toBe('s32'); + }); + + it('forbids invalid size prop', () => { + expect(icon.$options.props.size.validator(NaN)).toBeFalsy(); + expect(icon.$options.props.size.validator(0)).toBeFalsy(); + expect(icon.$options.props.size.validator(9001)).toBeFalsy(); }); it('should properly render img css', function () { const classList = icon.$el.classList; - const containsSizeClass = classList.contains('s99'); + const containsSizeClass = classList.contains('s32'); const containsCustomClass = classList.contains('extraclasses'); expect(containsSizeClass).toBe(true); expect(containsCustomClass).toBe(true); diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js index 2cf4d8e00ed..24484796bf1 100644 --- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js +++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js @@ -16,7 +16,7 @@ describe('Issue Warning Component', () => { isLocked: true, }); - expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-lock'); + expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/lock$/); expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is locked. Only project members can comment.'); }); }); @@ -27,7 +27,7 @@ describe('Issue Warning Component', () => { isConfidential: true, }); - expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-eye-slash'); + expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/eye-slash$/); expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This is a confidential issue. Your comment will not be visible to the public.'); }); }); @@ -39,7 +39,7 @@ describe('Issue Warning Component', () => { isConfidential: true, }); - expect(vm.$el.querySelector('i')).toBeFalsy(); + expect(vm.$el.querySelector('.icon')).toBeFalsy(); expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is confidential and locked. People without permission will never get a notification and won\'t be able to comment.'); }); }); diff --git a/spec/javascripts/vue_shared/components/loading_button_spec.js b/spec/javascripts/vue_shared/components/loading_button_spec.js index 97c8a08fcdd..49bf8ee6f7c 100644 --- a/spec/javascripts/vue_shared/components/loading_button_spec.js +++ b/spec/javascripts/vue_shared/components/loading_button_spec.js @@ -66,6 +66,23 @@ describe('LoadingButton', function () { }); }); + describe('container class', () => { + it('should default to btn btn-align-content', () => { + vm = mountComponent(LoadingButton, {}); + expect(vm.$el.classList.contains('btn')).toEqual(true); + expect(vm.$el.classList.contains('btn-align-content')).toEqual(true); + }); + + it('should be configurable through props', () => { + vm = mountComponent(LoadingButton, { + containerClass: 'test-class', + }); + expect(vm.$el.classList.contains('btn')).toEqual(false); + expect(vm.$el.classList.contains('btn-align-content')).toEqual(false); + expect(vm.$el.classList.contains('test-class')).toEqual(true); + }); + }); + describe('click callback prop', () => { it('calls given callback when normal', () => { vm = mountComponent(LoadingButton, { @@ -81,7 +98,6 @@ describe('LoadingButton', function () { it('does not call given callback when disabled because of loading', () => { vm = mountComponent(LoadingButton, { loading: true, - indeterminate: true, }); spyOn(vm, '$emit'); diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js new file mode 100644 index 00000000000..818ef0af3c2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import toolbar from '~/vue_shared/components/markdown/toolbar.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('toolbar', () => { + let vm; + const Toolbar = Vue.extend(toolbar); + const props = { + markdownDocsPath: '', + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('user can attach file', () => { + beforeEach(() => { + vm = mountComponent(Toolbar, props); + }); + + it('should render uploading-container', () => { + expect(vm.$el.querySelector('.uploading-container')).not.toBeNull(); + }); + }); + + describe('user cannot attach file', () => { + beforeEach(() => { + vm = mountComponent(Toolbar, Object.assign({}, props, { + canAttachFile: false, + })); + }); + + it('should not render uploading-container', () => { + expect(vm.$el.querySelector('.uploading-container')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js new file mode 100644 index 00000000000..78e7d747b92 --- /dev/null +++ b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js @@ -0,0 +1,61 @@ +import Vue from 'vue'; +import navigationTabs from '~/vue_shared/components/navigation_tabs.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('navigation tabs component', () => { + let vm; + let Component; + let data; + + beforeEach(() => { + data = [ + { + name: 'All', + scope: 'all', + count: 1, + isActive: true, + }, + { + name: 'Pending', + scope: 'pending', + count: 0, + isActive: false, + }, + { + name: 'Running', + scope: 'running', + isActive: false, + }, + ]; + + Component = Vue.extend(navigationTabs); + vm = mountComponent(Component, { tabs: data, scope: 'pipelines' }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render tabs', () => { + expect(vm.$el.querySelectorAll('li').length).toEqual(data.length); + }); + + it('should render active tab', () => { + expect(vm.$el.querySelector('.active .js-pipelines-tab-all')).toBeDefined(); + }); + + it('should render badge', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all .badge').textContent.trim()).toEqual('1'); + expect(vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim()).toEqual('0'); + }); + + it('should not render badge', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null); + }); + + it('should trigger onTabClick', () => { + spyOn(vm, '$emit'); + vm.$el.querySelector('.js-pipelines-tab-pending').click(); + expect(vm.$emit).toHaveBeenCalledWith('onChangeTab', 'pending'); + }); +}); diff --git a/spec/javascripts/vue_shared/components/pikaday_spec.js b/spec/javascripts/vue_shared/components/pikaday_spec.js new file mode 100644 index 00000000000..47af9534737 --- /dev/null +++ b/spec/javascripts/vue_shared/components/pikaday_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import datePicker from '~/vue_shared/components/pikaday.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('datePicker', () => { + let vm; + beforeEach(() => { + const DatePicker = Vue.extend(datePicker); + vm = mountComponent(DatePicker, { + label: 'label', + }); + }); + + it('should render label text', () => { + expect(vm.$el.querySelector('.dropdown-toggle-text').innerText.trim()).toEqual('label'); + }); + + it('should show calendar', () => { + expect(vm.$el.querySelector('.pika-single')).toBeDefined(); + }); + + it('should toggle when dropdown is clicked', () => { + const hidePicker = jasmine.createSpy(); + vm.$on('hidePicker', hidePicker); + + vm.$el.querySelector('.dropdown-menu-toggle').click(); + expect(hidePicker).toHaveBeenCalled(); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js new file mode 100644 index 00000000000..cce53193870 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('collapsedCalendarIcon', () => { + let vm; + beforeEach(() => { + const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon); + vm = mountComponent(CollapsedCalendarIcon, { + containerClass: 'test-class', + text: 'text', + showIcon: false, + }); + }); + + it('should add class to container', () => { + expect(vm.$el.classList.contains('test-class')).toEqual(true); + }); + + it('should hide calendar icon if showIcon', () => { + expect(vm.$el.querySelector('.fa-calendar')).toBeNull(); + }); + + it('should render text', () => { + expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text'); + }); + + it('should emit click event when container is clicked', () => { + const click = jasmine.createSpy(); + vm.$on('click', click); + + vm.$el.click(); + expect(click).toHaveBeenCalled(); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js new file mode 100644 index 00000000000..20363e78094 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -0,0 +1,91 @@ +import Vue from 'vue'; +import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('collapsedGroupedDatePicker', () => { + let vm; + beforeEach(() => { + const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker); + vm = mountComponent(CollapsedGroupedDatePicker, { + showToggleSidebar: true, + }); + }); + + it('should render toggle sidebar if showToggleSidebar', (done) => { + expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined(); + + vm.showToggleSidebar = false; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull(); + done(); + }); + }); + + it('toggleCollapse events', () => { + const toggleCollapse = jasmine.createSpy(); + + beforeEach((done) => { + vm.minDate = new Date('07/17/2016'); + Vue.nextTick(done); + }); + + it('should emit when sidebar is toggled', () => { + vm.$el.querySelector('.gutter-toggle').click(); + expect(toggleCollapse).toHaveBeenCalled(); + }); + + it('should emit when collapsed-calendar-icon is clicked', () => { + vm.$el.querySelector('.sidebar-collapsed-icon').click(); + expect(toggleCollapse).toHaveBeenCalled(); + }); + }); + + describe('minDate and maxDate', () => { + beforeEach((done) => { + vm.minDate = new Date('07/17/2016'); + vm.maxDate = new Date('07/17/2017'); + Vue.nextTick(done); + }); + + it('should render both collapsed-calendar-icon', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + expect(icons.length).toEqual(2); + expect(icons[0].innerText.trim()).toEqual('Jul 17 2016'); + expect(icons[1].innerText.trim()).toEqual('Jul 17 2017'); + }); + }); + + describe('minDate', () => { + beforeEach((done) => { + vm.minDate = new Date('07/17/2016'); + Vue.nextTick(done); + }); + + it('should render minDate in collapsed-calendar-icon', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + expect(icons.length).toEqual(1); + expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016'); + }); + }); + + describe('maxDate', () => { + beforeEach((done) => { + vm.maxDate = new Date('07/17/2017'); + Vue.nextTick(done); + }); + + it('should render maxDate in collapsed-calendar-icon', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + expect(icons.length).toEqual(1); + expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017'); + }); + }); + + describe('no dates', () => { + it('should render None', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + expect(icons.length).toEqual(1); + expect(icons[0].innerText.trim()).toEqual('None'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js new file mode 100644 index 00000000000..926e11b4d30 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js @@ -0,0 +1,117 @@ +import Vue from 'vue'; +import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('sidebarDatePicker', () => { + let vm; + beforeEach(() => { + const SidebarDatePicker = Vue.extend(sidebarDatePicker); + vm = mountComponent(SidebarDatePicker, { + label: 'label', + isLoading: true, + }); + }); + + it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => { + const toggleCollapse = jasmine.createSpy(); + vm.$on('toggleCollapse', toggleCollapse); + + vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click(); + expect(toggleCollapse).toHaveBeenCalled(); + }); + + it('should render collapsed-calendar-icon', () => { + expect(vm.$el.querySelector('.sidebar-collapsed-icon')).toBeDefined(); + }); + + it('should render label', () => { + expect(vm.$el.querySelector('.title').innerText.trim()).toEqual('label'); + }); + + it('should render loading-icon when isLoading', () => { + expect(vm.$el.querySelector('.fa-spin')).toBeDefined(); + }); + + it('should render value when not editing', () => { + expect(vm.$el.querySelector('.value-content')).toBeDefined(); + }); + + it('should render None if there is no selectedDate', () => { + expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None'); + }); + + it('should render date-picker when editing', (done) => { + vm.editing = true; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.pika-label')).toBeDefined(); + done(); + }); + }); + + describe('editable', () => { + beforeEach((done) => { + vm.editable = true; + Vue.nextTick(done); + }); + + it('should render edit button', () => { + expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit'); + }); + + it('should enable editing when edit button is clicked', (done) => { + vm.isLoading = false; + Vue.nextTick(() => { + vm.$el.querySelector('.title .btn-blank').click(); + expect(vm.editing).toEqual(true); + done(); + }); + }); + }); + + it('should render date if selectedDate', (done) => { + vm.selectedDate = new Date('07/07/2017'); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017'); + done(); + }); + }); + + describe('selectedDate and editable', () => { + beforeEach((done) => { + vm.selectedDate = new Date('07/07/2017'); + vm.editable = true; + Vue.nextTick(done); + }); + + it('should render remove button if selectedDate and editable', () => { + expect(vm.$el.querySelector('.value-content .btn-blank').innerText.trim()).toEqual('remove'); + }); + + it('should emit saveDate when remove button is clicked', () => { + const saveDate = jasmine.createSpy(); + vm.$on('saveDate', saveDate); + + vm.$el.querySelector('.value-content .btn-blank').click(); + expect(saveDate).toHaveBeenCalled(); + }); + }); + + describe('showToggleSidebar', () => { + beforeEach((done) => { + vm.showToggleSidebar = true; + Vue.nextTick(done); + }); + + it('should render toggle-sidebar when showToggleSidebar', () => { + expect(vm.$el.querySelector('.title .gutter-toggle')).toBeDefined(); + }); + + it('should emit toggleCollapse when toggle sidebar is clicked', () => { + const toggleCollapse = jasmine.createSpy(); + vm.$on('toggleCollapse', toggleCollapse); + + vm.$el.querySelector('.title .gutter-toggle').click(); + expect(toggleCollapse).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js new file mode 100644 index 00000000000..752a9e89d50 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('toggleSidebar', () => { + let vm; + beforeEach(() => { + const ToggleSidebar = Vue.extend(toggleSidebar); + vm = mountComponent(ToggleSidebar, { + collapsed: true, + }); + }); + + it('should render << when collapsed', () => { + expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-left')).toEqual(true); + }); + + it('should render >> when collapsed', () => { + vm.collapsed = false; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-right')).toEqual(true); + }); + }); + + it('should emit toggle event when button clicked', () => { + const toggle = jasmine.createSpy(); + vm.$on('toggle', toggle); + vm.$el.click(); + + expect(toggle).toHaveBeenCalled(); + }); +}); diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/javascripts/vue_shared/components/toggle_button_spec.js new file mode 100644 index 00000000000..447d74d4e08 --- /dev/null +++ b/spec/javascripts/vue_shared/components/toggle_button_spec.js @@ -0,0 +1,91 @@ +import Vue from 'vue'; +import toggleButton from '~/vue_shared/components/toggle_button.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Toggle Button', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(toggleButton); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('render output', () => { + beforeEach(() => { + vm = mountComponent(Component, { + value: true, + name: 'foo', + }); + }); + + it('renders input with provided name', () => { + expect(vm.$el.querySelector('input').getAttribute('name')).toEqual('foo'); + }); + + it('renders input with provided value', () => { + expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true'); + }); + + it('renders Enabled and Disabled text data attributes', () => { + expect(vm.$el.querySelector('button').getAttribute('data-enabled-text')).toEqual('Enabled'); + expect(vm.$el.querySelector('button').getAttribute('data-disabled-text')).toEqual('Disabled'); + }); + }); + + describe('is-checked', () => { + beforeEach(() => { + vm = mountComponent(Component, { + value: true, + }); + + spyOn(vm, '$emit'); + }); + + it('renders is checked class', () => { + expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true); + }); + + it('emits change event when clicked', () => { + vm.$el.querySelector('button').click(); + + expect(vm.$emit).toHaveBeenCalledWith('change', false); + }); + }); + + describe('is-disabled', () => { + beforeEach(() => { + vm = mountComponent(Component, { + value: true, + disabledInput: true, + }); + spyOn(vm, '$emit'); + }); + + it('renders disabled button', () => { + expect(vm.$el.querySelector('button').classList.contains('is-disabled')).toEqual(true); + }); + + it('does not emit change event when clicked', () => { + vm.$el.querySelector('button').click(); + + expect(vm.$emit).not.toHaveBeenCalled(); + }); + }); + + describe('is-loading', () => { + beforeEach(() => { + vm = mountComponent(Component, { + value: true, + isLoading: true, + }); + }); + + it('renders loading class', () => { + expect(vm.$el.querySelector('button').classList.contains('is-loading')).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 7047053d131..45a0bb0650f 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,77 +1,93 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, max-len */ /* global Mousetrap */ import Dropzone from 'dropzone'; import ZenMode from '~/zen_mode'; -(function() { - var enterZen, escapeKeydown, exitZen; - - describe('ZenMode', function() { - var fixtureName = 'merge_requests/merge_request_with_comment.html.raw'; - preloadFixtures(fixtureName); - beforeEach(function() { - loadFixtures(fixtureName); - spyOn(Dropzone, 'forElement').and.callFake(function() { - return { - enable: function() { - return true; - } - }; - // Stub Dropzone.forElement(...).enable() - }); - this.zen = new ZenMode(); - // Set this manually because we can't actually scroll the window - return this.zen.scroll_position = 456; +describe('ZenMode', () => { + let zen; + const fixtureName = 'merge_requests/merge_request_with_comment.html.raw'; + + preloadFixtures(fixtureName); + + function enterZen() { + $('.notes-form .js-zen-enter').click(); + } + + function exitZen() { + $('.notes-form .js-zen-leave').click(); + } + + function escapeKeydown() { + $('.notes-form textarea').trigger($.Event('keydown', { + keyCode: 27, + })); + } + + beforeEach(() => { + loadFixtures(fixtureName); + + spyOn(Dropzone, 'forElement').and.callFake(() => ({ + enable: () => true, + })); + zen = new ZenMode(); + + // Set this manually because we can't actually scroll the window + zen.scroll_position = 456; + }); + + describe('on enter', () => { + it('pauses Mousetrap', () => { + spyOn(Mousetrap, 'pause'); + enterZen(); + expect(Mousetrap.pause).toHaveBeenCalled(); }); - describe('on enter', function() { - it('pauses Mousetrap', function() { - spyOn(Mousetrap, 'pause'); - enterZen(); - return expect(Mousetrap.pause).toHaveBeenCalled(); - }); - return it('removes textarea styling', function() { - $('.notes-form textarea').attr('style', 'height: 400px'); - enterZen(); - return expect($('.notes-form textarea')).not.toHaveAttr('style'); - }); + + it('removes textarea styling', () => { + $('.notes-form textarea').attr('style', 'height: 400px'); + enterZen(); + expect($('.notes-form textarea')).not.toHaveAttr('style'); }); - describe('in use', function() { - beforeEach(function() { - return enterZen(); - }); - return it('exits on Escape', function() { - escapeKeydown(); - return expect($('.notes-form .zen-backdrop')).not.toHaveClass('fullscreen'); - }); + }); + + describe('in use', () => { + beforeEach(enterZen); + + it('exits on Escape', () => { + escapeKeydown(); + expect($('.notes-form .zen-backdrop')).not.toHaveClass('fullscreen'); + }); + }); + + describe('on exit', () => { + beforeEach(enterZen); + + it('unpauses Mousetrap', () => { + spyOn(Mousetrap, 'unpause'); + exitZen(); + expect(Mousetrap.unpause).toHaveBeenCalled(); }); - return describe('on exit', function() { - beforeEach(function() { - return enterZen(); - }); - it('unpauses Mousetrap', function() { - spyOn(Mousetrap, 'unpause'); - exitZen(); - return expect(Mousetrap.unpause).toHaveBeenCalled(); - }); - return it('restores the scroll position', function() { - spyOn(this.zen, 'scrollTo'); - exitZen(); - return expect(this.zen.scrollTo).toHaveBeenCalled(); - }); + + it('restores the scroll position', () => { + spyOn(zen, 'scrollTo'); + exitZen(); + expect(zen.scrollTo).toHaveBeenCalled(); }); }); - enterZen = function() { - return $('.notes-form .js-zen-enter').click(); - }; + describe('enabling dropzone', () => { + beforeEach(() => { + enterZen(); + }); - exitZen = function() { - return $('.notes-form .js-zen-leave').click(); - }; + it('should not call dropzone if element is not dropzone valid', () => { + $('.div-dropzone').addClass('js-invalid-dropzone'); + exitZen(); + expect(Dropzone.forElement).not.toHaveBeenCalled(); + }); - escapeKeydown = function() { - return $('.notes-form textarea').trigger($.Event('keydown', { - keyCode: 27 - })); - }; -}).call(window); + it('should call dropzone if element is dropzone valid', () => { + $('.div-dropzone').removeClass('js-invalid-dropzone'); + exitZen(); + expect(Dropzone.forElement).toHaveBeenCalled(); + }); + }); +}); |
