diff options
Diffstat (limited to 'spec/javascripts')
106 files changed, 1551 insertions, 660 deletions
diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js index df1b2c9960b..a143fc827d5 100644 --- a/spec/javascripts/blob/notebook/index_spec.js +++ b/spec/javascripts/blob/notebook/index_spec.js @@ -45,7 +45,7 @@ describe('iPython notebook renderer', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('does not show loading icon', () => { @@ -96,7 +96,7 @@ describe('iPython notebook renderer', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('does not show loading icon', () => { @@ -127,7 +127,7 @@ describe('iPython notebook renderer', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('does not show loading icon', () => { diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 4e73fa1fe87..80a598e63bd 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -55,7 +55,7 @@ describe('Board card', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('returns false when detailIssue is empty', () => { diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 7c5888b6d82..a5fcb10b9dd 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -60,7 +60,7 @@ describe('Board list component', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('renders component', () => { @@ -154,6 +154,18 @@ describe('Board list component', () => { }); }); + it('sets data attribute with invalid id', (done) => { + component.showCount = true; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-count').getAttribute('data-issue-id'), + ).toBe('-1'); + + done(); + }); + }); + it('shows how many more issues to load', (done) => { component.showCount = true; component.list.issuesSize = 20; @@ -172,9 +184,9 @@ describe('Board list component', () => { component.$refs.list.style.height = '100px'; component.$refs.list.style.overflow = 'scroll'; - for (let i = 0; i < 19; i += 1) { - const issue = component.list.issues[0]; - issue.id += 1; + for (let i = 1; i < 20; i += 1) { + const issue = Object.assign({}, component.list.issues[0]); + issue.id += i; component.list.issues.push(issue); } diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index c62c537841c..e204985f039 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -58,7 +58,7 @@ describe('Issue boards new issue form', () => { afterEach(() => { vm.$destroy(); - mock.reset(); + mock.restore(); }); it('calls submit if submit button is clicked', (done) => { diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 49fb20f4c84..8411f4dd8a6 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -35,7 +35,7 @@ describe('Store', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('starts with a blank state', () => { diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 8ef221257be..278155c585e 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -45,6 +45,9 @@ describe('Issue card component', () => { component = new Vue({ el: document.querySelector('.test-container'), + components: { + 'issue-card': gl.issueBoards.IssueCardInner, + }, data() { return { list, @@ -53,9 +56,6 @@ describe('Issue card component', () => { rootPath: '/', }; }, - components: { - 'issue-card': gl.issueBoards.IssueCardInner, - }, template: ` <issue-card :issue="issue" diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index 645ce831b53..34964b20b05 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; - +import _ from 'underscore'; import '~/boards/models/issue'; import '~/boards/models/label'; import '~/boards/models/list'; @@ -30,7 +30,7 @@ describe('List model', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('gets issues when created', (done) => { diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 9ae2d535398..0671facb285 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,5 +1,6 @@ /* global BoardService */ /* eslint-disable comma-dangle, no-unused-vars, quote-props */ +import _ from 'underscore'; export const listObj = { id: 300, diff --git a/spec/javascripts/boards/utils/query_data_spec.js b/spec/javascripts/boards/utils/query_data_spec.js new file mode 100644 index 00000000000..922215ffc1d --- /dev/null +++ b/spec/javascripts/boards/utils/query_data_spec.js @@ -0,0 +1,27 @@ +import queryData from '~/boards/utils/query_data'; + +describe('queryData', () => { + it('parses path for label with trailing +', () => { + expect( + queryData('label_name[]=label%2B', {}), + ).toEqual({ + label_name: ['label+'], + }); + }); + + it('parses path for milestone with trailing +', () => { + expect( + queryData('milestone_title=A%2B', {}), + ).toEqual({ + milestone_title: 'A+', + }); + }); + + it('parses path for search terms with spaces', () => { + expect( + queryData('search=two+words', {}), + ).toEqual({ + search: 'two words', + }); + }); +}); diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 9fc047b1f5e..0afe09d87bc 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import pipelinesTable from '~/commit/pipelines/pipelines_table.vue'; @@ -9,9 +10,10 @@ describe('Pipelines table in Commits and Merge requests', () => { preloadFixtures(jsonFixtureName); beforeEach(() => { - PipelinesTable = Vue.extend(pipelinesTable); const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + + PipelinesTable = Vue.extend(pipelinesTable); + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); }); describe('successful request', () => { diff --git a/spec/javascripts/commit_merge_requests_spec.js b/spec/javascripts/commit_merge_requests_spec.js new file mode 100644 index 00000000000..3466ef51ea8 --- /dev/null +++ b/spec/javascripts/commit_merge_requests_spec.js @@ -0,0 +1,60 @@ +import * as CommitMergeRequests from '~/commit_merge_requests'; + +describe('CommitMergeRequests', () => { + describe('createContent', () => { + it('should return created content', () => { + const content1 = CommitMergeRequests.createContent([{ iid: 1, path: '/path1', title: 'foo' }, { iid: 2, path: '/path2', title: 'baz' }])[0]; + expect(content1.tagName).toEqual('SPAN'); + expect(content1.childElementCount).toEqual(4); + + const content2 = CommitMergeRequests.createContent([])[0]; + expect(content2.tagName).toEqual('SPAN'); + expect(content2.childElementCount).toEqual(0); + expect(content2.innerText).toEqual('No related merge requests found'); + }); + }); + + describe('getHeaderText', () => { + it('should return header text', () => { + expect(CommitMergeRequests.getHeaderText(0, 1)).toEqual('1 merge request'); + expect(CommitMergeRequests.getHeaderText(0, 2)).toEqual('2 merge requests'); + expect(CommitMergeRequests.getHeaderText(1, 1)).toEqual(','); + expect(CommitMergeRequests.getHeaderText(1, 2)).toEqual(','); + }); + }); + + describe('createHeader', () => { + it('should return created header', () => { + const header = CommitMergeRequests.createHeader(0, 1)[0]; + expect(header.tagName).toEqual('SPAN'); + expect(header.innerText).toEqual('1 merge request'); + }); + }); + + describe('createItem', () => { + it('should return created item', () => { + const item = CommitMergeRequests.createItem({ iid: 1, path: '/path', title: 'foo' })[0]; + expect(item.tagName).toEqual('SPAN'); + expect(item.childElementCount).toEqual(2); + expect(item.children[0].tagName).toEqual('A'); + expect(item.children[1].tagName).toEqual('SPAN'); + }); + }); + + describe('createLink', () => { + it('should return created link', () => { + const link = CommitMergeRequests.createLink({ iid: 1, path: '/path', title: 'foo' })[0]; + expect(link.tagName).toEqual('A'); + expect(link.href).toMatch(/\/path$/); + expect(link.innerText).toEqual('!1'); + }); + }); + + describe('createTitle', () => { + it('should return created title', () => { + const title = CommitMergeRequests.createTitle({ iid: 1, path: '/path', title: 'foo' })[0]; + expect(title.tagName).toEqual('SPAN'); + expect(title.innerText).toEqual('foo'); + }); + }); +}); diff --git a/spec/javascripts/create_item_dropdown_spec.js b/spec/javascripts/create_item_dropdown_spec.js new file mode 100644 index 00000000000..c8b00a4f553 --- /dev/null +++ b/spec/javascripts/create_item_dropdown_spec.js @@ -0,0 +1,106 @@ +import CreateItemDropdown from '~/create_item_dropdown'; + +const DROPDOWN_ITEM_DATA = [{ + title: 'one', + id: 'one', + text: 'one', +}, { + title: 'two', + id: 'two', + text: 'two', +}, { + title: 'three', + id: 'three', + text: 'three', +}]; + +describe('CreateItemDropdown', () => { + preloadFixtures('static/create_item_dropdown.html.raw'); + + let $wrapperEl; + + beforeEach(() => { + loadFixtures('static/create_item_dropdown.html.raw'); + $wrapperEl = $('.js-create-item-dropdown-fixture-root'); + + // eslint-disable-next-line no-new + new CreateItemDropdown({ + $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'), + defaultToggleLabel: 'All variables', + fieldName: 'variable[environment]', + getData: (term, callback) => { + callback(DROPDOWN_ITEM_DATA); + }, + }); + }); + + afterEach(() => { + $wrapperEl.remove(); + }); + + it('should have a dropdown item for each piece of data', () => { + // Get the data in the dropdown + $('.js-dropdown-menu-toggle').click(); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); + }); + + describe('created items', () => { + const NEW_ITEM_TEXT = 'foobarbaz'; + + function createItemAndClearInput(text) { + // Filter for the new item + $wrapperEl.find('.dropdown-input-field') + .val(text) + .trigger('input'); + + // Create the new item + const $createButton = $wrapperEl.find('.js-dropdown-create-new-item'); + $createButton.click(); + + // Clear out the filter + $wrapperEl.find('.dropdown-input-field') + .val('') + .trigger('input'); + } + + beforeEach(() => { + // Open the dropdown + $('.js-dropdown-menu-toggle').click(); + + // Filter for the new item + $wrapperEl.find('.dropdown-input-field') + .val(NEW_ITEM_TEXT) + .trigger('input'); + }); + + it('create new item button should include the filter text', () => { + expect($wrapperEl.find('.js-dropdown-create-new-item code').text()).toEqual(NEW_ITEM_TEXT); + }); + + it('should update the dropdown with the newly created item', () => { + // Create the new item + const $createButton = $wrapperEl.find('.js-dropdown-create-new-item'); + $createButton.click(); + + expect($wrapperEl.find('.dropdown-toggle-text').text()).toEqual(NEW_ITEM_TEXT); + expect($wrapperEl.find('input[name="variable[environment]"]').val()).toEqual(NEW_ITEM_TEXT); + }); + + it('should include newly created item in dropdown list', () => { + createItemAndClearInput(NEW_ITEM_TEXT); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(1 + DROPDOWN_ITEM_DATA.length); + expect($($itemEls.get(DROPDOWN_ITEM_DATA.length)).text()).toEqual(NEW_ITEM_TEXT); + }); + + it('should not duplicate an item when trying to create an existing item', () => { + createItemAndClearInput(DROPDOWN_ITEM_DATA[0].text); + + const $itemEls = $wrapperEl.find('.js-dropdown-content a'); + expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); + }); + }); +}); diff --git a/spec/javascripts/cycle_analytics/banner_spec.js b/spec/javascripts/cycle_analytics/banner_spec.js index fb6b7fee168..64a76a6ee5f 100644 --- a/spec/javascripts/cycle_analytics/banner_spec.js +++ b/spec/javascripts/cycle_analytics/banner_spec.js @@ -20,8 +20,9 @@ describe('Cycle analytics banner', () => { expect( vm.$el.querySelector('h4').textContent.trim(), ).toEqual('Introducing Cycle Analytics'); + expect( - vm.$el.querySelector('p').textContent.trim(), + vm.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.'); expect( vm.$el.querySelector('a').textContent.trim(), diff --git a/spec/javascripts/cycle_analytics/total_time_component_spec.js b/spec/javascripts/cycle_analytics/total_time_component_spec.js index 31b65fd1cde..ad0fc38a856 100644 --- a/spec/javascripts/cycle_analytics/total_time_component_spec.js +++ b/spec/javascripts/cycle_analytics/total_time_component_spec.js @@ -23,7 +23,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('3 days 4 hrs'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('3 days 4 hrs'); }); it('should render information for hours and minutes', () => { @@ -34,7 +34,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('4 hrs 35 mins'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('4 hrs 35 mins'); }); it('should render information for seconds', () => { @@ -44,7 +44,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('45 s'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('45 s'); }); }); diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js index 0ca9290d3d2..b870f87eab9 100644 --- a/spec/javascripts/deploy_keys/components/app_spec.js +++ b/spec/javascripts/deploy_keys/components/app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import eventHub from '~/deploy_keys/eventhub'; import deployKeysApp from '~/deploy_keys/components/app.vue'; diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index 2f28c5bbf01..b7aadf604a4 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -53,18 +53,24 @@ describe('Deploy keys key', () => { ).toBe('Remove'); }); - it('shows write access text when key has write access', (done) => { - vm.deployKey.can_push = true; + it('shows write access title when key has write access', (done) => { + vm.deployKey.deploy_keys_projects[0].can_push = true; Vue.nextTick(() => { expect( - vm.$el.querySelector('.write-access-allowed'), - ).not.toBeNull(); - - expect( - vm.$el.querySelector('.write-access-allowed').textContent.trim(), + vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'), ).toBe('Write access allowed'); + done(); + }); + }); + + it('does not show write access title when key has write access', (done) => { + vm.deployKey.deploy_keys_projects[0].can_push = false; + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'), + ).toBe('Read access only'); done(); }); }); diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index d02adb25b4e..a41a4e5a3f7 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import environmentsComponent from '~/environments/components/environments_app.vue'; import { environment, folder } from './mock_data'; diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 4ea4d9d7499..a085074d312 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; import { environmentsList } from '../mock_data'; diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index 2ecb64d84b5..0684c3498a2 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; diff --git a/spec/javascripts/fixtures/create_item_dropdown.html.haml b/spec/javascripts/fixtures/create_item_dropdown.html.haml new file mode 100644 index 00000000000..d4d91b93caf --- /dev/null +++ b/spec/javascripts/fixtures/create_item_dropdown.html.haml @@ -0,0 +1,13 @@ +.js-create-item-dropdown-fixture-root + %input{ name: 'variable[environment]', type: 'hidden' } + = dropdown_tag('some label', + options: { toggle_class: 'js-dropdown-menu-toggle', + content_class: 'js-dropdown-content', + filter: true, + dropdown_class: "dropdown-menu-selectable", + footer_content: true }) do + %ul.dropdown-footer-list + %li + %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item" } + Create wildcard + %code diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml index 85ee61f0b54..0161c0550d1 100644 --- a/spec/javascripts/fixtures/pipelines.html.haml +++ b/spec/javascripts/fixtures/pipelines.html.haml @@ -7,4 +7,6 @@ "new-pipeline-path" => 'foo', "can-create-pipeline" => 'true', "has-ci" => 'foo', - "ci-lint-path" => 'foo' } } + "ci-lint-path" => 'foo', + "reset-cache-path" => 'foo' } } + diff --git a/spec/javascripts/fixtures/search.rb b/spec/javascripts/fixtures/search.rb new file mode 100644 index 00000000000..703cd3d49fa --- /dev/null +++ b/spec/javascripts/fixtures/search.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe SearchController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + render_views + + before(:all) do + clean_frontend_fixtures('search/') + end + + it 'search/show.html.raw' do |example| + get :show + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js index 97e3ab682c5..7198dbd4cf2 100644 --- a/spec/javascripts/flash_spec.js +++ b/spec/javascripts/flash_spec.js @@ -183,11 +183,15 @@ describe('Flash', () => { }); it('adds flash element into container', () => { - flash('test'); + flash('test', 'alert', document, null, false, true); expect( document.querySelector('.flash-alert'), ).not.toBeNull(); + + expect( + document.body.className, + ).toContain('flash-shown'); }); it('adds flash into specified parent', () => { @@ -220,13 +224,17 @@ describe('Flash', () => { }); it('removes element after clicking', () => { - flash('test', 'alert', document, null, false); + flash('test', 'alert', document, null, false, true); document.querySelector('.flash-alert').click(); expect( document.querySelector('.flash-alert'), ).toBeNull(); + + expect( + document.body.className, + ).not.toContain('flash-shown'); }); describe('with actionConfig', () => { diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index a3fa07d5bc2..eb9330f5e5b 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -167,30 +167,26 @@ describe('Fly out sidebar navigation', () => { describe('mouseEnterTopItems', () => { beforeEach(() => { - jasmine.clock().install(); - el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute; top: 0; left: 100px; height: 200px;"></div>'; }); - afterEach(() => { - jasmine.clock().uninstall(); - }); - - it('shows sub-items after 0ms if no menu is open', () => { + it('shows sub-items after 0ms if no menu is open', (done) => { mouseEnterTopItems(el); expect( getHideSubItemsInterval(), ).toBe(0); - jasmine.clock().tick(0); + setTimeout(() => { + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); - expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).toBe('block'); + done(); + }); }); - it('shows sub-items after 300ms if a menu is currently open', () => { + it('shows sub-items after 300ms if a menu is currently open', (done) => { documentMouseMove({ clientX: el.getBoundingClientRect().left, clientY: el.getBoundingClientRect().top, @@ -203,17 +199,19 @@ describe('Fly out sidebar navigation', () => { clientY: el.getBoundingClientRect().top + 10, }); - mouseEnterTopItems(el); + mouseEnterTopItems(el, 0); expect( getHideSubItemsInterval(), ).toBe(300); - jasmine.clock().tick(300); + setTimeout(() => { + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); - expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).toBe('block'); + done(); + }); }); }); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 97e39f6411b..8338efe915b 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -256,6 +256,36 @@ describe('AppComponent', () => { }); }); + describe('showLeaveGroupModal', () => { + it('caches candidate group (as props) which is to be left', () => { + const group = Object.assign({}, mockParentGroupItem); + expect(vm.targetGroup).toBe(null); + expect(vm.targetParentGroup).toBe(null); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.targetGroup).not.toBe(null); + expect(vm.targetParentGroup).not.toBe(null); + }); + + it('updates props which show modal confirmation dialog', () => { + const group = Object.assign({}, mockParentGroupItem); + expect(vm.showModal).toBeFalsy(); + expect(vm.groupLeaveConfirmationMessage).toBe(''); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.showModal).toBeTruthy(); + expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`); + }); + }); + + describe('hideLeaveGroupModal', () => { + it('hides modal confirmation which is shown before leaving the group', () => { + const group = Object.assign({}, mockParentGroupItem); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.showModal).toBeTruthy(); + vm.hideLeaveGroupModal(); + expect(vm.showModal).toBeFalsy(); + }); + }); + describe('leaveGroup', () => { let groupItem; let childGroupItem; @@ -265,21 +295,24 @@ describe('AppComponent', () => { groupItem.children = mockChildren; childGroupItem = groupItem.children[0]; groupItem.isChildrenLoading = false; + vm.targetGroup = childGroupItem; + vm.targetParentGroup = groupItem; }); - it('should leave group and remove group item from tree', (done) => { + it('hides modal confirmation leave group and remove group item from tree', (done) => { const notice = `You left the "${childGroupItem.fullName}" group.`; spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ notice })); spyOn(vm.store, 'removeGroup').and.callThrough(); spyOn(window, 'Flash'); spyOn($, 'scrollTo'); - vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); - expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + vm.leaveGroup(); + expect(vm.showModal).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); setTimeout(() => { expect($.scrollTo).toHaveBeenCalledWith(0); - expect(vm.store.removeGroup).toHaveBeenCalledWith(childGroupItem, groupItem); + expect(vm.store.removeGroup).toHaveBeenCalledWith(vm.targetGroup, vm.targetParentGroup); expect(window.Flash).toHaveBeenCalledWith(notice, 'notice'); done(); }, 0); @@ -291,13 +324,13 @@ describe('AppComponent', () => { spyOn(vm.store, 'removeGroup').and.callThrough(); spyOn(window, 'Flash'); - vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); + vm.leaveGroup(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(childGroupItem.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); done(); }, 0); }); @@ -309,12 +342,12 @@ describe('AppComponent', () => { spyOn(window, 'Flash'); vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(childGroupItem.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); done(); }, 0); }); @@ -364,7 +397,7 @@ describe('AppComponent', () => { Vue.nextTick(() => { expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); - expect(eventHub.$on).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); newVm.$destroy(); @@ -404,7 +437,7 @@ describe('AppComponent', () => { Vue.nextTick(() => { expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); - expect(eventHub.$off).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); done(); @@ -439,5 +472,14 @@ describe('AppComponent', () => { done(); }); }); + + it('renders modal confirmation dialog', () => { + vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; + vm.showModal = true; + const modalDialogEl = vm.$el.querySelector('.modal'); + expect(modalDialogEl).not.toBe(null); + expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); + expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); + }); }); }); diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js index 7a5c1da4d1d..acccbe639c4 100644 --- a/spec/javascripts/groups/components/item_actions_spec.js +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -26,38 +26,12 @@ describe('ItemActionsComponent', () => { vm.$destroy(); }); - describe('computed', () => { - describe('leaveConfirmationMessage', () => { - it('should return appropriate string for leave group confirmation', () => { - expect(vm.leaveConfirmationMessage).toBe('Are you sure you want to leave the "platform / hardware" group?'); - }); - }); - }); - describe('methods', () => { describe('onLeaveGroup', () => { - it('should change `modalStatus` prop to `true` which shows confirmation dialog', () => { - expect(vm.modalStatus).toBeFalsy(); - vm.onLeaveGroup(); - expect(vm.modalStatus).toBeTruthy(); - }); - }); - - describe('leaveGroup', () => { - it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { - spyOn(eventHub, '$emit'); - vm.modalStatus = true; - vm.leaveGroup(true); - expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); - }); - - it('should change `modalStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { + it('emits `showLeaveGroupModal` event with `group` and `parentGroup` props', () => { spyOn(eventHub, '$emit'); - vm.modalStatus = true; - vm.leaveGroup(false); - expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).not.toHaveBeenCalled(); + vm.onLeaveGroup(); + expect(eventHub.$emit).toHaveBeenCalledWith('showLeaveGroupModal', vm.group, vm.parentGroup); }); }); }); @@ -78,7 +52,8 @@ describe('ItemActionsComponent', () => { expect(editBtn.getAttribute('href')).toBe(group.editPath); expect(editBtn.getAttribute('aria-label')).toBe('Edit group'); expect(editBtn.dataset.originalTitle).toBe('Edit group'); - expect(editBtn.querySelector('i.fa.fa-cogs')).toBeDefined(); + expect(editBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(editBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#settings'); newVm.$destroy(); }); @@ -94,17 +69,10 @@ describe('ItemActionsComponent', () => { expect(leaveBtn.getAttribute('href')).toBe(group.leavePath); expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group'); expect(leaveBtn.dataset.originalTitle).toBe('Leave this group'); - expect(leaveBtn.querySelector('i.fa.fa-sign-out')).toBeDefined(); + expect(leaveBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(leaveBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#leave'); newVm.$destroy(); }); - - it('should show modal dialog when `modalStatus` is set to `true`', () => { - vm.modalStatus = true; - const modalDialogEl = vm.$el.querySelector('.modal'); - expect(modalDialogEl).toBeDefined(); - expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); - expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); - }); }); }); diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js index 686b8eaed31..1415ffb7eb3 100644 --- a/spec/javascripts/helpers/class_spec_helper_spec.js +++ b/spec/javascripts/helpers/class_spec_helper_spec.js @@ -3,7 +3,7 @@ import './class_spec_helper'; describe('ClassSpecHelper', () => { - describe('itShouldBeAStaticMethod', function () { + describe('itShouldBeAStaticMethod', () => { beforeEach(() => { class TestClass { instanceMethod() { this.prop = 'val'; } @@ -14,23 +14,5 @@ describe('ClassSpecHelper', () => { }); ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod'); - - it('should have a defined spec', () => { - expect(ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod').description).toBe('should be a static method'); - }); - - it('should pass for a static method', () => { - const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod'); - expect(spec.status()).toBe('passed'); - }); - - it('should fail for an instance method', (done) => { - const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'instanceMethod'); - spec.resultCallback = (result) => { - expect(result.status).toBe('failed'); - done(); - }; - spec.execute(); - }); }); }); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 1454ca52018..1c9f48028f2 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -59,7 +59,7 @@ describe('Issuable output', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); realtimeRequestCount = 0; vm.poll.stop(); @@ -218,6 +218,39 @@ describe('Issuable output', () => { }); }); + describe('shows dialog when issue has unsaved changed', () => { + it('confirms on title change', (done) => { + vm.showForm = true; + vm.state.titleText = 'title has changed'; + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + Vue.nextTick(() => { + expect(e.returnValue).not.toBeNull(); + done(); + }); + }); + + it('confirms on description change', (done) => { + vm.showForm = true; + vm.state.descriptionText = 'description has changed'; + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + Vue.nextTick(() => { + expect(e.returnValue).not.toBeNull(); + done(); + }); + }); + + it('does nothing when nothing has changed', (done) => { + const e = { returnValue: null }; + vm.handleBeforeUnloadEvent(e); + Vue.nextTick(() => { + expect(e.returnValue).toBeNull(); + done(); + }); + }); + }); + describe('error when updating', () => { beforeEach(() => { spyOn(window, 'Flash').and.callThrough(); diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/javascripts/issue_show/components/fields/description_template_spec.js index 2b7ee65094b..30441faf844 100644 --- a/spec/javascripts/issue_show/components/fields/description_template_spec.js +++ b/spec/javascripts/issue_show/components/fields/description_template_spec.js @@ -1,7 +1,5 @@ import Vue from 'vue'; import descriptionTemplate from '~/issue_show/components/fields/description_template.vue'; -import '~/templates/issuable_template_selector'; -import '~/templates/issuable_template_selectors'; describe('Issue description template component', () => { let vm; diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js index 000b53af016..50ce019c32a 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -1,7 +1,5 @@ import Vue from 'vue'; import formComponent from '~/issue_show/components/form.vue'; -import '~/templates/issuable_template_selector'; -import '~/templates/issuable_template_selectors'; describe('Inline edit form component', () => { let vm; diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index b740c9ed893..feb341d22e6 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -52,11 +52,6 @@ describe('Job', () => { expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false); expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); }); - - it('displays the remove date correctly', () => { - const removeDateElement = document.querySelector('.js-artifacts-remove'); - expect(removeDateElement.innerText.trim()).toBe('1 year remaining'); - }); }); describe('running build', () => { diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js index 4a210faa017..a9df0418d5d 100644 --- a/spec/javascripts/jobs/header_spec.js +++ b/spec/javascripts/jobs/header_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import headerComponent from '~/jobs/components/header.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Job details header', () => { let HeaderComponent; @@ -30,27 +31,45 @@ describe('Job details header', () => { email: 'foo@bar.com', avatar_url: 'link', }, + started: '2018-01-08T09:48:27.319Z', new_issue_path: 'path', }, isLoading: false, }; - vm = new HeaderComponent({ propsData: props }).$mount(); + vm = mountComponent(HeaderComponent, props); }); afterEach(() => { vm.$destroy(); }); - it('should render provided job information', () => { - expect( - vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), - ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); + describe('triggered job', () => { + beforeEach(() => { + vm = mountComponent(HeaderComponent, props); + }); + + it('should render provided job information', () => { + expect( + vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); + }); + + it('should render new issue link', () => { + expect( + vm.$el.querySelector('.js-new-issue').getAttribute('href'), + ).toEqual(props.job.new_issue_path); + }); }); - it('should render new issue link', () => { - expect( - vm.$el.querySelector('.js-new-issue').getAttribute('href'), - ).toEqual(props.job.new_issue_path); + describe('created job', () => { + it('should render created key', () => { + props.job.started = false; + vm = mountComponent(HeaderComponent, props); + + expect( + vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('failed Job #123 created 3 weeks ago by Foo'); + }); }); }); diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js index 3069a0cd60e..ca5c9cf87e4 100644 --- a/spec/javascripts/jobs/job_details_mediator_spec.js +++ b/spec/javascripts/jobs/job_details_mediator_spec.js @@ -12,6 +12,10 @@ describe('JobMediator', () => { mock = new MockAdapter(axios); }); + afterEach(() => { + mock.restore(); + }); + it('should set defaults', () => { expect(mediator.store).toBeDefined(); expect(mediator.service).toBeDefined(); @@ -24,10 +28,6 @@ describe('JobMediator', () => { mock.onGet().reply(200, job, {}); }); - afterEach(() => { - mock.restore(); - }); - it('should store received data', (done) => { mediator.fetchJob(); setTimeout(() => { diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 1f46c225071..69a23d7b2f3 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -62,4 +62,14 @@ describe('text_utility', () => { expect(textUtils.slugify('João')).toEqual('joão'); }); }); + + describe('stripHtml', () => { + it('replaces html tag with the default replacement', () => { + expect(textUtils.stripHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.'); + }); + + it('replaces html tags with the provided replacement', () => { + expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .'); + }); + }); }); diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index e983e4de3fc..5d0ee91d977 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 2f02c11482f..bae3219b043 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -19,17 +19,24 @@ import IssuablesHelper from '~/helpers/issuables_helper'; $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - return it('submits an ajax request on tasklist:changed', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { + + it('submits an ajax request on tasklist:changed', (done) => { + spyOn(jQuery, 'ajax').and.callFake((req) => { expect(req.type).toBe('PATCH'); expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`); - return expect(req.data.merge_request.description).not.toBe(null); + expect(req.data.merge_request.description).not.toBe(null); + done(); }); - return $('.js-task-list-field').trigger('tasklist:changed'); + + $('.js-task-list-field').trigger('tasklist:changed'); }); }); describe('class constructor', () => { + beforeEach(() => { + spyOn(jQuery, 'ajax').and.stub(); + }); + it('calls .initCloseReopenReport', () => { spyOn(IssuablesHelper, 'initCloseReopenReport'); @@ -63,8 +70,8 @@ import IssuablesHelper from '~/helpers/issuables_helper'; beforeEach(() => { loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); this.el = document.querySelector('.js-issuable-actions'); - const merge = new MergeRequest(); - merge.hideCloseButton(); + new MergeRequest(); // eslint-disable-line no-new + MergeRequest.hideCloseButton(); }); it('hides the dropdown close item and selects the next item', () => { @@ -83,8 +90,7 @@ import IssuablesHelper from '~/helpers/issuables_helper'; beforeEach(() => { loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); this.el = document.querySelector('.js-issuable-actions'); - const merge = new MergeRequest(); - merge.hideCloseButton(); + MergeRequest.hideCloseButton(); }); it('hides the close button', () => { diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 9885b8a790f..eb8f6bbe50d 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -38,7 +38,7 @@ describe('Dashboard', () => { }); afterEach(() => { - mock.reset(); + mock.restore(); }); it('shows up a loading state', (done) => { diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js index bf6ada8185e..d07db871d69 100644 --- a/spec/javascripts/monitoring/graph/deployment_spec.js +++ b/spec/javascripts/monitoring/graph/deployment_spec.js @@ -11,168 +11,38 @@ const createComponent = (propsData) => { }; describe('MonitoringDeployment', () => { - const reducedDeploymentData = [deploymentData[0]]; - reducedDeploymentData[0].ref = reducedDeploymentData[0].ref.name; - reducedDeploymentData[0].xPos = 10; - reducedDeploymentData[0].time = new Date(reducedDeploymentData[0].created_at); describe('Methods', () => { - it('refText shows the ref when a tag is available', () => { - reducedDeploymentData[0].tag = '1.0'; - const component = createComponent({ - showDeployInfo: false, - deploymentData: reducedDeploymentData, - graphWidth: 440, - graphHeight: 300, - graphHeightOffset: 120, - }); - - expect( - component.refText(reducedDeploymentData[0]), - ).toEqual(reducedDeploymentData[0].ref); - }); - - it('refText shows the sha when no tag is available', () => { - reducedDeploymentData[0].tag = null; - const component = createComponent({ - showDeployInfo: false, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.refText(reducedDeploymentData[0]), - ).toContain('f5bcd1'); - }); - - it('nameDeploymentClass creates a class with the prefix deploy-info-', () => { + it('should contain a hidden gradient', () => { const component = createComponent({ - showDeployInfo: false, - deploymentData: reducedDeploymentData, + showDeployInfo: true, + deploymentData, graphHeight: 300, graphWidth: 440, graphHeightOffset: 120, }); - expect( - component.nameDeploymentClass(reducedDeploymentData[0]), - ).toContain('deploy-info'); + expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull(); }); it('transformDeploymentGroup translates an available deployment', () => { const component = createComponent({ showDeployInfo: false, - deploymentData: reducedDeploymentData, + deploymentData, graphHeight: 300, graphWidth: 440, graphHeightOffset: 120, }); expect( - component.transformDeploymentGroup(reducedDeploymentData[0]), + component.transformDeploymentGroup({ xPos: 16 }), ).toContain('translate(11, 20)'); }); - it('hides the deployment flag', () => { - reducedDeploymentData[0].showDeploymentFlag = false; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphWidth: 440, - graphHeight: 300, - graphHeightOffset: 120, - }); - - expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull(); - }); - - it('positions the flag to the left when the xPos is too far right', () => { - reducedDeploymentData[0].showDeploymentFlag = false; - reducedDeploymentData[0].xPos = 250; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphWidth: 440, - graphHeight: 300, - graphHeightOffset: 120, - }); - - expect( - component.positionFlag(reducedDeploymentData[0]), - ).toBeLessThan(0); - }); - - it('shows the deployment flag', () => { - reducedDeploymentData[0].showDeploymentFlag = true; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.$el.querySelector('.js-deploy-info-box').style.display, - ).not.toEqual('display: none;'); - }); - - it('contains date, refs and the "deployed" text', () => { - reducedDeploymentData[0].showDeploymentFlag = true; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.$el.querySelectorAll('.deploy-info-text'), - ).toContainText('Deployed'); - - expect( - component.$el.querySelectorAll('.deploy-info-text'), - ).toContainText('Wed, May 31'); - - expect( - component.$el.querySelectorAll('.deploy-info-text'), - ).toContainText(component.refText(reducedDeploymentData[0])); - }); - - it('contains a link to the commit contents', () => { - reducedDeploymentData[0].showDeploymentFlag = true; - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect( - component.$el.querySelectorAll('.deploy-info-text-link')[0].parentElement.getAttribute('xlink:href'), - ).not.toEqual(''); - }); - - it('should contain a hidden gradient', () => { - const component = createComponent({ - showDeployInfo: true, - deploymentData: reducedDeploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull(); - }); - describe('Computed props', () => { it('calculatedHeight', () => { const component = createComponent({ showDeployInfo: true, - deploymentData: reducedDeploymentData, + deploymentData, graphHeight: 300, graphWidth: 440, graphHeightOffset: 120, diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index 8ee1171419d..2d474e9092f 100644 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import GraphFlag from '~/monitoring/components/graph/flag.vue'; +import { deploymentData } from '../mock_data'; const createComponent = (propsData) => { const Component = Vue.extend(GraphFlag); @@ -9,11 +10,6 @@ const createComponent = (propsData) => { }).$mount(); }; -function getCoordinate(component, selector, coordinate) { - const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate); - return parseInt(coordinateVal, 10); -} - const defaultValuesComponent = { currentXCoordinate: 200, currentYCoordinate: 100, @@ -25,31 +21,111 @@ const defaultValuesComponent = { graphHeight: 300, graphHeightOffset: 120, showFlagContent: true, + realPixelRatio: 1, + timeSeries: [{ + values: [{ + time: new Date('2017-06-04T18:17:33.501Z'), + value: '1.49609375', + }], + }], + unitOfDisplay: 'ms', + currentDataIndex: 0, + legendTitle: 'Average', +}; + +const deploymentFlagData = { + ...deploymentData[0], + ref: deploymentData[0].ref.name, + xPos: 10, + time: new Date(deploymentData[0].created_at), }; describe('GraphFlag', () => { - it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => { - const component = createComponent(defaultValuesComponent); + let component; - expect(getCoordinate(component, '.selected-metric-line', 'x1')) - .toEqual(component.currentXCoordinate); - expect(getCoordinate(component, '.selected-metric-line', 'x2')) - .toEqual(component.currentXCoordinate); + it('has a line at the currentXCoordinate', () => { + component = createComponent(defaultValuesComponent); + + expect(component.$el.style.left) + .toEqual(`${70 + component.currentXCoordinate}px`); }); - it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => { - const component = createComponent(defaultValuesComponent); + describe('Deployment flag', () => { + it('shows a deployment flag when deployment data provided', () => { + const deploymentFlagComponent = createComponent({ + ...defaultValuesComponent, + deploymentFlagData, + }); + + expect( + deploymentFlagComponent.$el.querySelector('.popover-title'), + ).toContainText('Deployed'); + }); + + it('contains the ref when a tag is available', () => { + const deploymentFlagComponent = createComponent({ + ...defaultValuesComponent, + deploymentFlagData: { + ...deploymentFlagData, + sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + tag: true, + ref: '1.0', + }, + }); + + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).toContainText('f5bcd1d9'); + + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).toContainText('1.0'); + }); + + it('does not contain the ref when a tag is unavailable', () => { + const deploymentFlagComponent = createComponent({ + ...defaultValuesComponent, + deploymentFlagData: { + ...deploymentFlagData, + sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + tag: false, + ref: '1.0', + }, + }); + + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).toContainText('f5bcd1d9'); - const svg = component.$el.querySelector('.rect-text-metric'); - expect(svg.tagName).toEqual('svg'); - expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition); + expect( + deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), + ).not.toContainText('1.0'); + }); }); describe('Computed props', () => { - it('calculatedHeight', () => { - const component = createComponent(defaultValuesComponent); + beforeEach(() => { + component = createComponent(defaultValuesComponent); + }); + + it('formatTime', () => { + expect(component.formatTime).toMatch(/\d:17PM/); + }); + + it('formatDate', () => { + expect(component.formatDate).toEqual('Sun, Jun 4'); + }); + + it('cursorStyle', () => { + expect(component.cursorStyle).toEqual({ + top: '20px', + left: '270px', + height: '180px', + }); + }); - expect(component.calculatedHeight).toEqual(180); + it('flagOrientation', () => { + expect(component.flagOrientation).toEqual('left'); }); }); }); diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js index a88e9ed3d99..02304bf5d7d 100644 --- a/spec/javascripts/notebook/cells/markdown_spec.js +++ b/spec/javascripts/notebook/cells/markdown_spec.js @@ -42,6 +42,18 @@ describe('Markdown component', () => { expect(vm.$el.querySelector('.markdown h1')).not.toBeNull(); }); + it('sanitizes output', (done) => { + Object.assign(cell, { + source: ['[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n'], + }); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('a')).toBeNull(); + + done(); + }); + }); + describe('katex', () => { beforeEach(() => { json = getJSONFixture('blob/notebook/math.json'); diff --git a/spec/javascripts/notebook/cells/output/html_sanitize_tests.js b/spec/javascripts/notebook/cells/output/html_sanitize_tests.js new file mode 100644 index 00000000000..d587573fc9e --- /dev/null +++ b/spec/javascripts/notebook/cells/output/html_sanitize_tests.js @@ -0,0 +1,66 @@ +export default { + 'protocol-based JS injection: simple, no spaces': { + input: '<a href="javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces before': { + input: '<a href="javascript :alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces after': { + input: '<a href="javascript: alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces before and after': { + input: '<a href="javascript : alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: preceding colon': { + input: '<a href=":javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: UTF-8 encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long UTF-8 encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long UTF-8 encoding without semicolons': { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: hex encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long hex encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: hex encoding without semicolons': { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: null char': { + input: '<a href=java\0script:alert("XSS")>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: invalid URL char': { + input: '<img src=java\script:alert("XSS")>', // eslint-disable-line no-useless-escape + output: '<img>', + }, + 'protocol-based JS injection: Unicode': { + input: '<a href="\u0001java\u0003script:alert(\'XSS\')">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: spaces and entities': { + input: '<a href="  javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'img on error': { + input: '<img src="x" onerror="alert(document.domain)" />', + output: '<img src="x">', + }, +}; diff --git a/spec/javascripts/notebook/cells/output/html_spec.js b/spec/javascripts/notebook/cells/output/html_spec.js new file mode 100644 index 00000000000..9c5385f2922 --- /dev/null +++ b/spec/javascripts/notebook/cells/output/html_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import htmlOutput from '~/notebook/cells/output/html.vue'; +import sanitizeTests from './html_sanitize_tests'; + +describe('html output cell', () => { + function createComponent(rawCode) { + const Component = Vue.extend(htmlOutput); + + return new Component({ + propsData: { + rawCode, + }, + }).$mount(); + } + + describe('sanitizes output', () => { + Object.keys(sanitizeTests).forEach((key) => { + it(key, () => { + const test = sanitizeTests[key]; + const vm = createComponent(test.input); + const outputEl = [...vm.$el.querySelectorAll('div')].pop(); + + expect(outputEl.innerHTML).toEqual(test.output); + + vm.$destroy(); + }); + }); + }); +}); diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index 20e352dd8bd..104d03377b6 100644 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -139,13 +139,21 @@ describe('issue_comment_form component', () => { }); describe('event enter', () => { - it('should save note when cmd/ctrl+enter is pressed', () => { + it('should save note when cmd+enter is pressed', () => { spyOn(vm, 'handleSave').and.callThrough(); vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true)); expect(vm.handleSave).toHaveBeenCalled(); }); + + it('should save note when ctrl+enter is pressed', () => { + spyOn(vm, 'handleSave').and.callThrough(); + vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; + vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect(vm.handleSave).toHaveBeenCalled(); + }); }); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 7c8d6685ee1..36c56cd3862 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index 86e9e2a32a9..f841a408d09 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -69,13 +69,20 @@ describe('issue_note_form component', () => { }); describe('enter', () => { - it('should submit note', () => { + it('should save note when cmd+enter is pressed', () => { spyOn(vm, 'handleUpdate').and.callThrough(); vm.$el.querySelector('textarea').value = 'Foo'; vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true)); expect(vm.handleUpdate).toHaveBeenCalled(); }); + it('should save note when ctrl+enter is pressed', () => { + spyOn(vm, 'handleUpdate').and.callThrough(); + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect(vm.handleUpdate).toHaveBeenCalled(); + }); }); }); diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index c8a6cb7e612..cb63b64724d 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -1,4 +1,4 @@ - +import _ from 'underscore'; import Vue from 'vue'; import store from '~/notes/stores'; import issueNote from '~/notes/components/noteable_note.vue'; diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 6b608adff15..b020a1020df 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -29,7 +29,6 @@ export const noteableDataMock = { can_create_note: true, can_update: true, }, - deleted_at: null, description: '', due_date: null, human_time_estimate: null, @@ -283,7 +282,6 @@ export const loggedOutnoteableData = { "updated_by_id": 1, "created_at": "2017-02-07T10:11:18.395Z", "updated_at": "2017-08-08T10:22:51.564Z", - "deleted_at": null, "time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 167f074fb9b..a40821a5693 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,4 +1,5 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ +import _ from 'underscore'; import * as urlUtils from '~/lib/utils/url_utility'; import 'autosize'; import '~/gl_form'; diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/javascripts/oauth_remember_me_spec.js index f90e0093d25..b24563f738b 100644 --- a/spec/javascripts/oauth_remember_me_spec.js +++ b/spec/javascripts/oauth_remember_me_spec.js @@ -1,4 +1,4 @@ -import OAuthRememberMe from '~/oauth_remember_me'; +import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me'; describe('OAuthRememberMe', () => { preloadFixtures('static/oauth_remember_me.html.raw'); diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js index 7f6b5873011..d2386077aa6 100644 --- a/spec/javascripts/abuse_reports_spec.js +++ b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js @@ -1,5 +1,5 @@ import '~/lib/utils/text_utility'; -import AbuseReports from '~/abuse_reports'; +import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports'; describe('Abuse Reports', () => { const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js new file mode 100644 index 00000000000..440a6585d57 --- /dev/null +++ b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; + +import axios from '~/lib/utils/axios_utils'; +import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue'; +import * as urlUtility from '~/lib/utils/url_utility'; + +import mountComponent from '../../../../../helpers/vue_mount_component_helper'; + +describe('stop_jobs_modal.vue', () => { + const props = { + url: `${gl.TEST_HOST}/stop_jobs_modal.vue/stopAll`, + }; + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + beforeEach(() => { + const Component = Vue.extend(stopJobsModal); + vm = mountComponent(Component, props); + }); + + describe('onSubmit', () => { + it('stops jobs and redirects to overview page', (done) => { + const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`; + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + spyOn(axios, 'post').and.callFake((url) => { + expect(url).toBe(props.url); + return Promise.resolve({ + request: { + responseURL, + }, + }); + }); + + vm.onSubmit() + .then(() => { + expect(redirectSpy).toHaveBeenCalledWith(responseURL); + }) + .then(done) + .catch(done.fail); + }); + + it('displays error if stopping jobs failed', (done) => { + const dummyError = new Error('stopping jobs failed'); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + spyOn(axios, 'post').and.callFake((url) => { + expect(url).toBe(props.url); + return Promise.reject(dummyError); + }); + + vm.onSubmit() + .then(done.fail) + .catch((error) => { + expect(error).toBe(dummyError); + expect(redirectSpy).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 48620898357..d010d897642 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -13,7 +13,7 @@ describe('Pipelines Async Button', () => { propsData: { endpoint: '/foo', title: 'Foo', - icon: 'fa fa-foo', + icon: 'repeat', cssClass: 'bar', }, }).$mount(); @@ -23,8 +23,8 @@ describe('Pipelines Async Button', () => { expect(component.$el.tagName).toEqual('BUTTON'); }); - it('should render the provided icon', () => { - expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo'); + it('should render svg icon', () => { + expect(component.$el.querySelector('svg')).not.toBeNull(); }); it('should render the provided title', () => { diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index 6611b74594f..97f04844b3a 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -24,11 +24,11 @@ describe('Pipelines Empty State', () => { expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect( - component.$el.querySelector('p').textContent, + component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Continous Integration can help catch bugs by running your tests automatically'); expect( - component.$el.querySelector('p').textContent, + component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Continuous Deployment can help you deliver code to your product environment'); }); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 35e36e9c353..c3dc7b53d0f 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -61,14 +61,14 @@ describe('pipeline graph job component', () => { it('it should render status and name', () => { component = mountComponent(JobComponent, { job: { - id: 4256, + id: 4257, name: 'test', status: { icon: 'icon_status_success', text: 'passed', label: 'passed', group: 'success', - details_path: '/root/ci-mock/builds/4256', + details_path: '/root/ci-mock/builds/4257', has_details: false, }, }, @@ -118,7 +118,7 @@ describe('pipeline graph job component', () => { it('should not render status label when it is not provided', () => { component = mountComponent(JobComponent, { job: { - id: 4256, + id: 4258, name: 'test', status: { icon: 'icon_status_success', @@ -132,7 +132,7 @@ describe('pipeline graph job component', () => { it('should not render status label when it is provided', () => { component = mountComponent(JobComponent, { job: { - id: 4256, + id: 4259, name: 'test', status: { icon: 'icon_status_success', diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js index 063ab53681b..f744f1af5e6 100644 --- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js +++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js @@ -4,18 +4,18 @@ import stageColumnComponent from '~/pipelines/components/graph/stage_column_comp describe('stage column component', () => { let component; const mockJob = { - id: 4256, + id: 4250, name: 'test', status: { icon: 'icon_status_success', text: 'passed', label: 'passed', group: 'success', - details_path: '/root/ci-mock/builds/4256', + details_path: '/root/ci-mock/builds/4250', action: { icon: 'retry', title: 'Retry', - path: '/root/ci-mock/builds/4256/retry', + path: '/root/ci-mock/builds/4250/retry', method: 'post', }, }, @@ -24,10 +24,17 @@ describe('stage column component', () => { beforeEach(() => { const StageColumnComponent = Vue.extend(stageColumnComponent); + const mockJobs = []; + for (let i = 0; i < 3; i += 1) { + const mockedJob = Object.assign({}, mockJob); + mockedJob.id += i; + mockJobs.push(mockedJob); + } + component = new StageColumnComponent({ propsData: { title: 'foo', - jobs: [mockJob, mockJob, mockJob], + jobs: mockJobs, }, }).$mount(); }); diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js index f1697840fcd..09a0c14d96c 100644 --- a/spec/javascripts/pipelines/nav_controls_spec.js +++ b/spec/javascripts/pipelines/nav_controls_spec.js @@ -14,6 +14,7 @@ describe('Pipelines Nav Controls', () => { hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', + resetCachePath: 'foo', canCreatePipeline: true, }; @@ -31,6 +32,7 @@ describe('Pipelines Nav Controls', () => { hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', + resetCachePath: 'foo', canCreatePipeline: false, }; @@ -41,12 +43,31 @@ describe('Pipelines Nav Controls', () => { expect(component.$el.querySelector('.btn-create')).toEqual(null); }); + it('should render link for resetting runner caches', () => { + const mockData = { + newPipelinePath: 'foo', + hasCiEnabled: true, + helpPagePath: 'foo', + ciLintPath: 'foo', + resetCachePath: 'foo', + canCreatePipeline: false, + }; + + const component = new NavControlsComponent({ + propsData: mockData, + }).$mount(); + + expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches'); + expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath); + }); + it('should render link for CI lint', () => { const mockData = { newPipelinePath: 'foo', hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', + resetCachePath: 'foo', canCreatePipeline: true, }; @@ -54,8 +75,8 @@ describe('Pipelines Nav Controls', () => { propsData: mockData, }).$mount(); - expect(component.$el.querySelector('.btn-default').textContent).toContain('CI Lint'); - expect(component.$el.querySelector('.btn-default').getAttribute('href')).toEqual(mockData.ciLintPath); + expect(component.$el.querySelectorAll('.btn-default')[1].textContent).toContain('CI Lint'); + expect(component.$el.querySelectorAll('.btn-default')[1].getAttribute('href')).toEqual(mockData.ciLintPath); }); it('should render link to help page when CI is not enabled', () => { @@ -64,6 +85,7 @@ describe('Pipelines Nav Controls', () => { hasCiEnabled: false, helpPagePath: 'foo', ciLintPath: 'foo', + resetCachePath: 'foo', canCreatePipeline: true, }; @@ -81,6 +103,7 @@ describe('Pipelines Nav Controls', () => { hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', + resetCachePath: 'foo', canCreatePipeline: true, }; diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js index 9fec2f61f78..bc6413a159f 100644 --- a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js +++ b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import PipelineMediator from '~/pipelines/pipeline_details_mediatior'; diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 367b42cefb0..a99ebc4e51a 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index a9126d2f4e9..b3cbf9aba48 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -24,9 +24,10 @@ describe('Pipelines Table Row', () => { beforeEach(() => { const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); - pipelineWithoutAuthor = pipelines.find(p => p.id === 2); - pipelineWithoutCommit = pipelines.find(p => p.id === 3); + + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); + pipelineWithoutAuthor = pipelines.find(p => p.user == null && p.commit !== null); + pipelineWithoutCommit = pipelines.find(p => p.user == null && p.commit == null); }); afterEach(() => { diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js index ca2f9163313..4fc3c08145e 100644 --- a/spec/javascripts/pipelines/pipelines_table_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_spec.js @@ -11,9 +11,10 @@ describe('Pipelines Table', () => { preloadFixtures(jsonFixtureName); beforeEach(() => { - PipelinesTableComponent = Vue.extend(pipelinesTableComp); const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + + PipelinesTableComponent = Vue.extend(pipelinesTableComp); + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); }); describe('table', () => { diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index 1b96b2e3d51..61c2f783acc 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import stage from '~/pipelines/components/stage.vue'; diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js index 2e94948cfb2..588b61196a5 100644 --- a/spec/javascripts/profile/account/components/delete_account_modal_spec.js +++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js @@ -51,7 +51,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).toHaveClass('disabled'); + expect(submitButton).toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).not.toHaveBeenCalled(); }) @@ -68,7 +68,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).not.toHaveClass('disabled'); + expect(submitButton).not.toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).toHaveBeenCalled(); }) @@ -101,7 +101,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).toHaveClass('disabled'); + expect(submitButton).toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).not.toHaveBeenCalled(); }) @@ -118,7 +118,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).not.toHaveClass('disabled'); + expect(submitButton).not.toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).toHaveBeenCalled(); }) diff --git a/spec/javascripts/projects/project_new_spec.js b/spec/javascripts/projects/project_new_spec.js index 850768f0e4f..c314ca8ab72 100644 --- a/spec/javascripts/projects/project_new_spec.js +++ b/spec/javascripts/projects/project_new_spec.js @@ -6,8 +6,12 @@ describe('New Project', () => { beforeEach(() => { setFixtures(` - <input id="project_import_url" /> - <input id="project_path" /> + <div class='toggle-import-form'> + <div class='import-url-data'> + <input id="project_import_url" /> + <input id="project_path" /> + </div> + </div> `); $projectImportUrl = $('#project_import_url'); @@ -25,7 +29,7 @@ describe('New Project', () => { it('does not change project path for disabled $projectImportUrl', () => { $projectImportUrl.attr('disabled', true); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -38,7 +42,7 @@ describe('New Project', () => { it('does not change project path if it is set by user', () => { $projectPath.keyup(); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -46,7 +50,7 @@ describe('New Project', () => { it('does not change project path for empty $projectImportUrl', () => { $projectImportUrl.val(''); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -54,7 +58,7 @@ describe('New Project', () => { it('does not change project path for whitespace $projectImportUrl', () => { $projectImportUrl.val(' '); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -62,7 +66,7 @@ describe('New Project', () => { it('does not change project path for $projectImportUrl without slashes', () => { $projectImportUrl.val('has-no-slash'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -70,7 +74,7 @@ describe('New Project', () => { it('changes project path to last $projectImportUrl component', () => { $projectImportUrl.val('/this/is/last'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('last'); }); @@ -78,7 +82,7 @@ describe('New Project', () => { it('ignores trailing slashes in $projectImportUrl', () => { $projectImportUrl.val('/has/trailing/slash/'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('slash'); }); @@ -86,7 +90,7 @@ describe('New Project', () => { it('ignores fragment identifier in $projectImportUrl', () => { $projectImportUrl.val('/this/has/a#fragment-identifier/'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('a'); }); @@ -94,7 +98,7 @@ describe('New Project', () => { it('ignores query string in $projectImportUrl', () => { $projectImportUrl.val('/url/with?query=string'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('with'); }); @@ -102,7 +106,7 @@ describe('New Project', () => { it('ignores trailing .git in $projectImportUrl', () => { $projectImportUrl.val('/repository.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('repository'); }); @@ -110,7 +114,7 @@ describe('New Project', () => { it('changes project path for HTTPS URL in $projectImportUrl', () => { $projectImportUrl.val('https://username:password@gitlab.company.com/group/project.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('project'); }); @@ -118,7 +122,7 @@ describe('New Project', () => { it('changes project path for SSH URL in $projectImportUrl', () => { $projectImportUrl.val('git@gitlab.com:gitlab-org/gitlab-ce.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('gitlab-ce'); }); diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 43e7d9e1224..6a8a85e3dfb 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import registry from '~/registry/components/app.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; @@ -89,7 +90,7 @@ describe('Registry List', () => { it('should render empty message', (done) => { setTimeout(() => { expect( - vm.$el.querySelector('p').textContent.trim(), + vm.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toEqual('No container images stored for this project. Add one by following the instructions above.'); done(); }, 0); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js index c4d3866c922..debde1bb357 100644 --- a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js @@ -12,7 +12,7 @@ describe('Multi-file editor commit sidebar list collapsed', () => { vm = createComponentWithStore(Component, store); - vm.$store.state.openFiles.push(file(), file()); + vm.$store.state.openFiles.push(file('file1'), file('file2')); vm.$store.state.openFiles[0].tempFile = true; vm.$store.state.openFiles.forEach((f) => { Object.assign(f, { diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js index fc7c9ae9dd7..4b20fdf70d6 100644 --- a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js @@ -10,7 +10,7 @@ describe('Multi-file editor commit sidebar list item', () => { beforeEach(() => { const Component = Vue.extend(listItem); - f = file(); + f = file('test-file'); vm = mountComponent(Component, { file: f, diff --git a/spec/javascripts/repo/components/ide_repo_tree_spec.js b/spec/javascripts/repo/components/ide_repo_tree_spec.js index b6f70f585cd..e3bbda514da 100644 --- a/spec/javascripts/repo/components/ide_repo_tree_spec.js +++ b/spec/javascripts/repo/components/ide_repo_tree_spec.js @@ -41,11 +41,11 @@ describe('IdeRepoTree', () => { expect(tbody.querySelector('.file')).toBeTruthy(); }); - it('renders 5 loading files if tree is loading', (done) => { - vm.$store.state.loading = true; + it('renders 3 loading files if tree is loading', (done) => { + vm.treeId = '123'; Vue.nextTick(() => { - expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5); + expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3); done(); }); diff --git a/spec/javascripts/repo/components/ide_spec.js b/spec/javascripts/repo/components/ide_spec.js index 20b8dc25dcb..acfd63eb8de 100644 --- a/spec/javascripts/repo/components/ide_spec.js +++ b/spec/javascripts/repo/components/ide_spec.js @@ -10,7 +10,9 @@ describe('ide component', () => { beforeEach(() => { const Component = Vue.extend(ide); - vm = createComponentWithStore(Component, store).$mount(); + vm = createComponentWithStore(Component, store, { + emptyStateSvgPath: 'svg', + }).$mount(); }); afterEach(() => { diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js index b001c1655b4..6efbbf6d75e 100644 --- a/spec/javascripts/repo/components/new_dropdown/index_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js @@ -57,16 +57,17 @@ describe('new dropdown component', () => { }); }); - describe('toggleModalOpen', () => { + describe('hideModal', () => { + beforeAll((done) => { + vm.openModal = true; + Vue.nextTick(done); + }); + it('closes modal after toggling', (done) => { - vm.toggleModalOpen(); + vm.hideModal(); Vue.nextTick() .then(() => { - expect(vm.$el.querySelector('.modal')).not.toBeNull(); - }) - .then(vm.toggleModalOpen) - .then(() => { expect(vm.$el.querySelector('.modal')).toBeNull(); }) .then(done) diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index cd93fb3ccbf..676ac09f2c9 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -29,7 +29,7 @@ describe('RepoCommitSection', () => { comp.$store.state.rightPanelCollapsed = false; comp.$store.state.currentBranch = 'master'; - comp.$store.state.openFiles = [file(), file()]; + comp.$store.state.openFiles = [file('file1'), file('file2')]; comp.$store.state.openFiles.forEach(f => Object.assign(f, { changed: true, content: 'testing', diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index 0810da87e80..27b55ed1f87 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -25,7 +25,7 @@ describe('RepoFile', () => { vm = new RepoFile({ store, propsData: { - file: file(), + file: file('t4'), }, }); spyOn(vm, 'timeFormated').and.returnValue(updated); @@ -39,7 +39,7 @@ describe('RepoFile', () => { it('does render if hasFiles is true and is loading tree', () => { vm = createComponent({ - file: file(), + file: file('t1'), }); expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); @@ -47,7 +47,7 @@ describe('RepoFile', () => { it('does not render commit message and datetime if mini', (done) => { vm = createComponent({ - file: file(), + file: file('t2'), }); vm.$store.state.openFiles.push(vm.file); @@ -61,7 +61,7 @@ describe('RepoFile', () => { it('fires clickFile when the link is clicked', () => { vm = createComponent({ - file: file(), + file: file('t3'), }); spyOn(vm, 'clickFile'); diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js index 507bca983df..933e8d3a06a 100644 --- a/spec/javascripts/repo/components/repo_tab_spec.js +++ b/spec/javascripts/repo/components/repo_tab_spec.js @@ -56,7 +56,7 @@ describe('RepoTab', () => { }); it('renders an fa-circle icon if tab is changed', () => { - const tab = file(); + const tab = file('changedFile'); tab.changed = true; vm = createComponent({ tab, @@ -68,7 +68,7 @@ describe('RepoTab', () => { describe('methods', () => { describe('closeTab', () => { it('does not close tab if is changed', (done) => { - const tab = file(); + const tab = file('closeFile'); tab.changed = true; tab.opened = true; vm = createComponent({ diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js index 0beaf643793..2c363364d70 100644 --- a/spec/javascripts/repo/components/repo_tabs_spec.js +++ b/spec/javascripts/repo/components/repo_tabs_spec.js @@ -4,7 +4,7 @@ import repoTabs from '~/ide/components/repo_tabs.vue'; import { file, resetStore } from '../helpers'; describe('RepoTabs', () => { - const openedFiles = [file(), file()]; + const openedFiles = [file('open1'), file('open2')]; let vm; function createComponent() { diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js index 8ce01d3bf12..e2d8f002e27 100644 --- a/spec/javascripts/repo/stores/actions/file_spec.js +++ b/spec/javascripts/repo/stores/actions/file_spec.js @@ -18,7 +18,7 @@ describe('Multi-file store file actions', () => { oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line - localFile = file(); + localFile = file('testFile'); localFile.active = true; localFile.opened = true; localFile.parentTreeUrl = 'parentTreeUrl'; @@ -81,7 +81,7 @@ describe('Multi-file store file actions', () => { }); it('sets next file as active', (done) => { - const f = file(); + const f = file('otherfile'); store.state.openFiles.push(f); expect(f.active).toBeFalsy(); @@ -119,7 +119,7 @@ describe('Multi-file store file actions', () => { }); it('calls scrollToTab', (done) => { - store.dispatch('setFileActive', file()) + store.dispatch('setFileActive', file('setThisActive')) .then(() => { expect(scrollToTabSpy).toHaveBeenCalled(); @@ -128,7 +128,7 @@ describe('Multi-file store file actions', () => { }); it('sets the file active', (done) => { - const localFile = file(); + const localFile = file('activeFile'); store.dispatch('setFileActive', localFile) .then(() => { @@ -139,7 +139,7 @@ describe('Multi-file store file actions', () => { }); it('returns early if file is already active', (done) => { - const localFile = file(); + const localFile = file('earlyActive'); localFile.active = true; store.dispatch('setFileActive', localFile) @@ -151,11 +151,11 @@ describe('Multi-file store file actions', () => { }); it('sets current active file to not active', (done) => { - const localFile = file(); + const localFile = file('currentActive'); localFile.active = true; store.state.openFiles.push(localFile); - store.dispatch('setFileActive', file()) + store.dispatch('setFileActive', file('newActive')) .then(() => { expect(localFile.active).toBeFalsy(); @@ -166,7 +166,7 @@ describe('Multi-file store file actions', () => { it('resets location.hash for line highlighting', (done) => { location.hash = 'test'; - store.dispatch('setFileActive', file()) + store.dispatch('setFileActive', file('otherActive')) .then(() => { expect(location.hash).not.toBe('test'); @@ -176,7 +176,7 @@ describe('Multi-file store file actions', () => { }); describe('getFileData', () => { - let localFile = file(); + let localFile; beforeEach(() => { spyOn(service, 'getFileData').and.returnValue(Promise.resolve({ @@ -194,10 +194,17 @@ describe('Multi-file store file actions', () => { }), })); - localFile = file(); + localFile = file('newCreate'); localFile.url = 'getFileDataURL'; }); + afterEach(() => { + store.dispatch('closeFile', { + file: localFile, + force: true, + }); + }); + it('calls the service', (done) => { store.dispatch('getFileData', localFile) .then(() => { @@ -268,7 +275,7 @@ describe('Multi-file store file actions', () => { beforeEach(() => { spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw')); - tmpFile = file(); + tmpFile = file('tmpFile'); }); it('calls getRawFileData service method', (done) => { @@ -294,7 +301,7 @@ describe('Multi-file store file actions', () => { let tmpFile; beforeEach(() => { - tmpFile = file(); + tmpFile = file('tmpFile'); }); it('updates file content', (done) => { diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js index 0b0d34f072a..8d830c67290 100644 --- a/spec/javascripts/repo/stores/actions_spec.js +++ b/spec/javascripts/repo/stores/actions_spec.js @@ -48,14 +48,14 @@ describe('Multi-file store actions', () => { describe('discardAllChanges', () => { beforeEach(() => { - store.state.openFiles.push(file()); + store.state.openFiles.push(file('discardAll')); store.state.openFiles[0].changed = true; }); }); describe('closeAllFiles', () => { beforeEach(() => { - store.state.openFiles.push(file()); + store.state.openFiles.push(file('closeAll')); store.state.openFiles[0].opened = true; }); @@ -97,7 +97,7 @@ describe('Multi-file store actions', () => { it('opens discard popup if there are changed files', (done) => { store.state.editMode = true; - store.state.openFiles.push(file()); + store.state.openFiles.push(file('discardChanges')); store.state.openFiles[0].changed = true; store.dispatch('toggleEditMode') @@ -111,7 +111,7 @@ describe('Multi-file store actions', () => { it('can force closed if there are changed files', (done) => { store.state.editMode = true; - store.state.openFiles.push(file()); + store.state.openFiles.push(file('forceClose')); store.state.openFiles[0].changed = true; store.dispatch('toggleEditMode', true) @@ -124,7 +124,7 @@ describe('Multi-file store actions', () => { }); it('discards file changes', (done) => { - const f = file(); + const f = file('discard'); store.state.editMode = true; store.state.openFiles.push(f); f.changed = true; @@ -285,8 +285,8 @@ describe('Multi-file store actions', () => { }); it('adds commit data to changed files', (done) => { - const changedFile = file(); - const f = file(); + const changedFile = file('changed'); + const f = file('newfile'); changedFile.changed = true; store.state.openFiles.push(changedFile, f); @@ -300,19 +300,6 @@ describe('Multi-file store actions', () => { }).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(() => { diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js index 947a60587df..6e204ef0404 100644 --- a/spec/javascripts/repo/stores/mutations/file_spec.js +++ b/spec/javascripts/repo/stores/mutations/file_spec.js @@ -117,7 +117,7 @@ describe('Multi-file store file mutations', () => { describe('CREATE_TMP_FILE', () => { it('adds file into parent tree', () => { - const f = file(); + const f = file('tmpFile'); mutations.CREATE_TMP_FILE(localState, { file: f, diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js index cf1248ba28b..e6ca8ea139e 100644 --- a/spec/javascripts/repo/stores/mutations/tree_spec.js +++ b/spec/javascripts/repo/stores/mutations/tree_spec.js @@ -57,7 +57,7 @@ describe('Multi-file store tree mutations', () => { describe('CREATE_TMP_TREE', () => { it('adds tree into parent tree', () => { - const tmpEntry = file(); + const tmpEntry = file('tmpTree'); mutations.CREATE_TMP_TREE(localState, { tmpEntry, diff --git a/spec/javascripts/search_spec.js b/spec/javascripts/search_spec.js new file mode 100644 index 00000000000..38e94d45e55 --- /dev/null +++ b/spec/javascripts/search_spec.js @@ -0,0 +1,40 @@ +import Api from '~/api'; +import Search from '~/pages/search/show/search'; + +describe('Search', () => { + const fixturePath = 'search/show.html.raw'; + const searchTerm = 'some search'; + const fillDropdownInput = (dropdownSelector) => { + const dropdownElement = document.querySelector(dropdownSelector).parentNode; + const inputElement = dropdownElement.querySelector('.dropdown-input-field'); + inputElement.value = searchTerm; + return inputElement; + }; + + preloadFixtures(fixturePath); + + beforeEach(() => { + loadFixtures(fixturePath); + new Search(); // eslint-disable-line no-new + }); + + it('requests groups from backend when filtering', (done) => { + spyOn(Api, 'groups').and.callFake((term) => { + expect(term).toBe(searchTerm); + done(); + }); + const inputElement = fillDropdownInput('.js-search-group-dropdown'); + + $(inputElement).trigger('input'); + }); + + it('requests projects from backend when filtering', (done) => { + spyOn(Api, 'projects').and.callFake((term) => { + expect(term).toBe(searchTerm); + done(); + }); + const inputElement = fillDropdownInput('.js-search-project-dropdown'); + + $(inputElement).trigger('input'); + }); +}); diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index 3b094d20838..7bc591d2d47 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -15,7 +15,6 @@ const RESPONSE_MAP = { updated_by_id: 1, created_at: '2017-02-02T21: 49: 49.664Z', updated_at: '2017-05-03T22: 26: 03.760Z', - deleted_at: null, time_estimate: 0, total_time_spent: 0, human_time_estimate: null, @@ -153,7 +152,6 @@ const RESPONSE_MAP = { updated_by_id: 1, created_at: '2017-06-27T19:54:42.437Z', updated_at: '2017-08-18T03:39:49.222Z', - deleted_at: null, time_estimate: 0, total_time_spent: 0, human_time_estimate: null, diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js index b97e24d9dcf..6bb6d639f24 100644 --- a/spec/javascripts/sidebar/sidebar_assignees_spec.js +++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees'; import SidebarMediator from '~/sidebar/sidebar_mediator'; diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index 9efd109b996..afa18cc127e 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import * as urlUtils from '~/lib/utils/url_utility'; import SidebarMediator from '~/sidebar/sidebar_mediator'; diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js index 8b0d51bbcc8..97f762d07a7 100644 --- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js +++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js index 9b33dd02fb9..79db05f04ed 100644 --- a/spec/javascripts/sidebar/subscriptions_spec.js +++ b/spec/javascripts/sidebar/subscriptions_spec.js @@ -20,23 +20,23 @@ describe('Subscriptions', function () { subscribed: undefined, }); - expect(vm.$refs.loadingButton.loading).toBe(true); - expect(vm.$refs.loadingButton.label).toBeUndefined(); + expect(vm.$refs.toggleButton.isLoading).toBe(true); + expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-loading'); }); - it('has "Subscribe" text when currently not subscribed', () => { + it('is toggled "off" when currently not subscribed', () => { vm = mountComponent(Subscriptions, { subscribed: false, }); - expect(vm.$refs.loadingButton.label).toBe('Subscribe'); + expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).not.toHaveClass('is-checked'); }); - it('has "Unsubscribe" text when currently not subscribed', () => { + it('is toggled "on" when currently subscribed', () => { vm = mountComponent(Subscriptions, { subscribed: true, }); - expect(vm.$refs.loadingButton.label).toBe('Unsubscribe'); + expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-checked'); }); }); diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js index a53e8a94d89..b1b03ef1e09 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js +++ b/spec/javascripts/signin_tabs_memoizer_spec.js @@ -1,5 +1,5 @@ import AccessorUtilities from '~/lib/utils/accessor'; -import SigninTabsMemoizer from '~/signin_tabs_memoizer'; +import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer'; (() => { describe('SigninTabsMemoizer', () => { @@ -53,6 +53,13 @@ import SigninTabsMemoizer from '~/signin_tabs_memoizer'; expect(memo.readData()).toEqual('#standard'); }); + it('overrides last selected tab with hash tag when given', () => { + window.location.hash = '#ldap'; + createMemoizer(); + + expect(memo.readData()).toEqual('#ldap'); + }); + describe('class constructor', () => { beforeEach(() => { memo = createMemoizer(); diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js index 1c87fcec245..7265e1b6cb5 100644 --- a/spec/javascripts/smart_interval_spec.js +++ b/spec/javascripts/smart_interval_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import SmartInterval from '~/smart_interval'; describe('SmartInterval', function () { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 6897c991066..2f6691df9cd 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -1,6 +1,5 @@ /* eslint-disable jasmine/no-global-setup */ import $ from 'jquery'; -import _ from 'underscore'; import 'jasmine-jquery'; import '~/commons'; @@ -31,7 +30,6 @@ jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; // globalize common libraries window.$ = window.jQuery = $; -window._ = _; // stub expected globals window.gl = window.gl || {}; diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index 59e16f0786e..35871dddf89 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -1,5 +1,5 @@ import * as urlUtils from '~/lib/utils/url_utility'; -import Todos from '~/todos'; +import Todos from '~/pages/dashboard/todos/index/todos'; import '~/lib/utils/common_utils'; describe('Todos', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js new file mode 100644 index 00000000000..66ecaa316c8 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js @@ -0,0 +1,115 @@ +import Vue from 'vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Merge request widget rebase component', () => { + let Component; + let vm; + beforeEach(() => { + Component = Vue.extend(component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('While rebasing', () => { + it('should show progress message', () => { + vm = mountComponent(Component, { + mr: { rebaseInProgress: true }, + service: {}, + }); + + expect( + vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(), + ).toContain('Rebase in progress'); + }); + }); + + describe('With permissions', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + }, + service: {}, + }); + }); + + it('it should render rebase button and warning message', () => { + const text = vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(); + expect(text).toContain('Fast-forward merge is not possible.'); + expect(text).toContain('Rebase the source branch onto the target branch or merge target'); + expect(text).toContain('branch into source branch to allow this merge request to be merged.'); + }); + + it('it should render error message when it fails', (done) => { + vm.rebasingError = 'Something went wrong!'; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(), + ).toContain('Something went wrong!'); + done(); + }); + }); + }); + + describe('Without permissions', () => { + it('should render a message explaining user does not have permissions', () => { + vm = mountComponent(Component, { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: false, + targetBranch: 'foo', + }, + service: {}, + }); + + const text = vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(); + + expect(text).toContain('Fast-forward merge is not possible.'); + expect(text).toContain('Rebase the source branch onto'); + expect(text).toContain('foo'); + expect(text).toContain('to allow this merge request to be merged.'); + }); + }); + + describe('methods', () => { + it('checkRebaseStatus', (done) => { + spyOn(eventHub, '$emit'); + vm = mountComponent(Component, { + mr: {}, + service: { + rebase() { + return Promise.resolve(); + }, + poll() { + return Promise.resolve({ + data: { + rebase_in_progress: false, + merge_error: null, + }, + }); + }, + }, + }); + + vm.rebase(); + + // Wait for the rebase request + vm.$nextTick() + // Wait for the polling request + .then(vm.$nextTick()) + // Wait for the eventHub to be called + .then(vm.$nextTick()) + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js index 4869fb17d96..f98ebdb38e6 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js @@ -1,18 +1,31 @@ import Vue from 'vue'; -import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived'; +import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetArchived', () => { - describe('template', () => { - it('should have correct elements', () => { - const Component = Vue.extend(archivedComponent); - const el = new Component({ - el: document.createElement('div'), - }).$el; + let vm; - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); - expect(el.querySelector('button').disabled).toBeTruthy(); - expect(el.innerText).toContain('This project is archived, write access has been disabled'); - }); + beforeEach(() => { + const Component = Vue.extend(archivedComponent); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a ci status failed icon', () => { + expect(vm.$el.querySelector('.ci-status-icon')).not.toBeNull(); + }); + + it('renders a disabled button', () => { + expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); + expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Merge'); + }); + + it('renders information', () => { + expect( + vm.$el.querySelector('.bold').textContent.trim(), + ).toEqual('This project is archived, write access has been disabled'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js index 6042d7384d5..95c94e95e3a 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -1,32 +1,47 @@ import Vue from 'vue'; -import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed'; - -const mergeError = 'This is the merge error'; +import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetAutoMergeFailed', () => { - describe('props', () => { - it('should have props', () => { - const mrProp = autoMergeFailedComponent.props.mr; + let vm; + const mergeError = 'This is the merge error'; - expect(mrProp.type instanceof Object).toBeTruthy(); - expect(mrProp.required).toBeTruthy(); + beforeEach(() => { + const Component = Vue.extend(autoMergeFailedComponent); + vm = mountComponent(Component, { + mr: { mergeError }, }); }); - describe('template', () => { - const Component = Vue.extend(autoMergeFailedComponent); - const vm = new Component({ - el: document.createElement('div'), - propsData: { - mr: { mergeError }, - }, - }); + afterEach(() => { + vm.$destroy(); + }); + + it('renders failed message', () => { + expect(vm.$el.textContent).toContain('This merge request failed to be merged automatically'); + }); + + it('renders merge error provided', () => { + expect(vm.$el.innerText).toContain(mergeError); + }); + + it('render refresh button', () => { + expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Refresh'); + }); + + it('emits event and shows loading icon when button is clicked', (done) => { + spyOn(eventHub, '$emit'); + vm.$el.querySelector('button').click(); + + expect(eventHub.$emit.calls.argsFor(0)[0]).toEqual('MRWidgetUpdateRequested'); - it('should have correct elements', () => { - expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeFalsy(); - expect(vm.$el.innerText).toContain('This merge request failed to be merged automatically'); - expect(vm.$el.innerText).toContain(mergeError); + Vue.nextTick(() => { + expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); + expect( + vm.$el.querySelector('button i').classList, + ).toContain('fa-spinner'); + done(); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js index 6b7aa935ad3..658cadddb81 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js @@ -1,19 +1,29 @@ import Vue from 'vue'; -import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking'; +import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetChecking', () => { - describe('template', () => { - it('should have correct elements', () => { - const Component = Vue.extend(checkingComponent); - const el = new Component({ - el: document.createElement('div'), - }).$el; + let Component; + let vm; - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); - expect(el.querySelector('button').disabled).toBeTruthy(); - expect(el.innerText).toContain('Checking ability to merge automatically'); - expect(el.querySelector('i')).toBeDefined(); - }); + beforeEach(() => { + Component = Vue.extend(checkingComponent); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders disabled button', () => { + expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); + }); + + it('renders loading icon', () => { + expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + }); + + it('renders information about merging', () => { + expect(vm.$el.querySelector('.media-body').textContent.trim()).toEqual('Checking ability to merge automatically'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js index 1bf97bbf093..51a34739ee9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -1,74 +1,58 @@ import Vue from 'vue'; -import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed'; - -const mr = { - targetBranch: 'good-branch', - targetBranchPath: '/good-branch', - metrics: { - mergedBy: {}, - mergedAt: 'mergedUpdatedAt', - closedBy: { - name: 'Fatih Acet', - username: 'fatihacet', - }, - closedAt: 'closedEventUpdatedAt', - readableMergedAt: '', - readableClosedAt: '', - }, - updatedAt: 'mrUpdatedAt', - closedAt: '1 day ago', -}; - -const createComponent = () => { - const Component = Vue.extend(closedComponent); - - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; +import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetClosed', () => { - describe('props', () => { - it('should have props', () => { - const mrProp = closedComponent.props.mr; - - expect(mrProp.type instanceof Object).toBeTruthy(); - expect(mrProp.required).toBeTruthy(); - }); + let vm; + + beforeEach(() => { + const Component = Vue.extend(closedComponent); + vm = mountComponent(Component, { mr: { + metrics: { + mergedBy: {}, + closedBy: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + mergedAt: 'Jan 24, 2018 1:02pm GMT+0000', + closedAt: 'Jan 24, 2018 1:02pm GMT+0000', + readableMergedAt: '', + readableClosedAt: 'less than a minute ago', + }, + targetBranchPath: '/twitter/flight/commits/so_long_jquery', + targetBranch: 'so_long_jquery', + } }); }); - describe('components', () => { - it('should have components added', () => { - expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined(); - }); + afterEach(() => { + vm.$destroy(); }); - describe('template', () => { - let vm; - let el; + it('renders warning icon', () => { + expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull(); + }); - beforeEach(() => { - vm = createComponent(); - el = vm.$el; - }); + it('renders closed by information with author and time', () => { + expect( + vm.$el.querySelector('.js-mr-widget-author').textContent.trim().replace(/\s\s+/g, ' '), + ).toContain( + 'Closed by Administrator less than a minute ago', + ); + }); - afterEach(() => { - vm.$destroy(); - }); + it('links to the user that closed the MR', () => { + expect(vm.$el.querySelector('.author-link').getAttribute('href')).toEqual('http://localhost:3000/root'); + }); - it('should have correct elements', () => { - expect(el.querySelector('h4').textContent).toContain('Closed by'); - expect(el.querySelector('h4').textContent).toContain(mr.metrics.closedBy.name); - expect(el.textContent).toContain('The changes were not merged into'); - expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath); - expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch); - }); + it('renders information about the changes not being merged', () => { + expect( + vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' '), + ).toContain('The changes were not merged into so_long_jquery'); + }); - it('should use closedEvent updatedAt as tooltip title', () => { - expect( - el.querySelector('time').getAttribute('title'), - ).toBe('closedEventUpdatedAt'); - }); + it('renders link for target branch', () => { + expect(vm.$el.querySelector('.label-branch').getAttribute('href')).toEqual('/twitter/flight/commits/so_long_jquery'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index 5d4c7ec09dc..a7d69fdcdb9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -1,105 +1,85 @@ import Vue from 'vue'; -import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts'; +import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue'; import mountComponent from '../../../helpers/vue_mount_component_helper'; -const ConflictsComponent = Vue.extend(conflictsComponent); -const path = '/conflicts'; - describe('MRWidgetConflicts', () => { - describe('props', () => { - it('should have props', () => { - const { mr } = conflictsComponent.props; + let Component; + let vm; + const path = '/conflicts'; - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - }); + beforeEach(() => { + Component = Vue.extend(conflictsComponent); }); - describe('template', () => { - describe('when allowed to merge', () => { - let vm; - - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - canMerge: true, - conflictResolutionPath: path, - }, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should tell you about conflicts without bothering other people', () => { - expect(vm.$el.textContent).toContain('There are merge conflicts'); - expect(vm.$el.textContent).not.toContain('ask someone with write access'); - }); - - it('should allow you to resolve the conflicts', () => { - const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); + afterEach(() => { + vm.$destroy(); + }); - expect(resolveButton.textContent).toContain('Resolve conflicts'); - expect(resolveButton.getAttribute('href')).toEqual(path); + describe('when allowed to merge', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + canMerge: true, + conflictResolutionPath: path, + }, }); + }); - it('should have merge buttons', () => { - const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); - const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button'); - - expect(mergeButton.textContent).toContain('Merge'); - expect(mergeButton.disabled).toBeTruthy(); - expect(mergeButton.classList.contains('btn-success')).toEqual(true); - expect(mergeLocallyButton.textContent).toContain('Merge locally'); - }); + it('should tell you about conflicts without bothering other people', () => { + expect(vm.$el.textContent).toContain('There are merge conflicts'); + expect(vm.$el.textContent).not.toContain('ask someone with write access'); }); - describe('when user does not have permission to merge', () => { - let vm; + it('should allow you to resolve the conflicts', () => { + const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - canMerge: false, - }, - }); - }); + expect(resolveButton.textContent).toContain('Resolve conflicts'); + expect(resolveButton.getAttribute('href')).toEqual(path); + }); - afterEach(() => { - vm.$destroy(); - }); + it('should have merge buttons', () => { + const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); + const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button'); - it('should show proper message', () => { - expect(vm.$el.textContent).toContain('ask someone with write access'); - }); + expect(mergeButton.textContent).toContain('Merge'); + expect(mergeButton.disabled).toBeTruthy(); + expect(mergeButton.classList.contains('btn-success')).toEqual(true); + expect(mergeLocallyButton.textContent).toContain('Merge locally'); + }); + }); - it('should not have action buttons', () => { - expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); - expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); - expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); + describe('when user does not have permission to merge', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + canMerge: false, + }, }); }); - describe('when fast-forward or semi-linear merge enabled', () => { - let vm; + it('should show proper message', () => { + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('ask someone with write access'); + }); - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - shouldBeRebased: true, - }, - }); - }); + it('should not have action buttons', () => { + expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); + expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); + expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); + }); + }); - afterEach(() => { - vm.$destroy(); + describe('when fast-forward or semi-linear merge enabled', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + shouldBeRebased: true, + }, }); + }); - it('should tell you to rebase locally', () => { - expect(vm.$el.textContent).toContain('Fast-forward merge is not possible.'); - expect(vm.$el.textContent).toContain('To merge this request, first rebase locally'); - }); + it('should tell you to rebase locally', () => { + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('Fast-forward merge is not possible.'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('To merge this request, first rebase locally'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 1127576617b..073f26cc78f 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -170,14 +170,14 @@ describe('MRWidgetReadyToMerge', () => { expect(vm.iconClass).toEqual('success'); }); - it('shows x for failed status', () => { + it('shows warning icon for failed status', () => { vm.mr.hasCI = true; - expect(vm.iconClass).toEqual('failed'); + expect(vm.iconClass).toEqual('warning'); }); - it('shows x for merge not allowed', () => { + it('shows warning icon for merge not allowed', () => { vm.mr.hasCI = true; - expect(vm.iconClass).toEqual('failed'); + expect(vm.iconClass).toEqual('warning'); }); }); @@ -371,6 +371,10 @@ describe('MRWidgetReadyToMerge', () => { }); }); + beforeEach(() => { + loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); + }); + it('should call start and stop polling when MR merged', (done) => { spyOn(eventHub, '$emit'); spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); @@ -392,6 +396,47 @@ describe('MRWidgetReadyToMerge', () => { }, 333); }); + it('updates status box', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + const statusBox = document.querySelector('.status-box'); + expect(statusBox.classList.contains('status-box-mr-merged')).toBeTruthy(); + expect(statusBox.textContent).toContain('Merged'); + + done(); + }); + }); + + it('hides close button', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + expect(document.querySelector('.btn-close').classList.contains('hidden')).toBeTruthy(); + + done(); + }); + }); + + it('updates merge request count badge', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + expect(document.querySelector('.js-merge-counter').textContent).toBe('0'); + + done(); + }); + }); + it('should continue polling until MR is merged', (done) => { spyOn(vm.service, 'poll').and.returnValue(returnPromise('some_other_state')); spyOn(vm, 'initiateRemoveSourceBranchPolling'); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index ca29c9fee32..ae494267659 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -14,7 +14,6 @@ export default { "updated_by_id": null, "created_at": "2017-04-07T12:27:26.718Z", "updated_at": "2017-04-07T15:39:25.852Z", - "deleted_at": null, "time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js new file mode 100644 index 00000000000..08e4e1f8337 --- /dev/null +++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('clipboard button', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(clipboardButton); + vm = mountComponent(Component, { + text: 'copy me', + title: 'Copy this value into Clipboard!', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a button for clipboard', () => { + expect(vm.$el.tagName).toEqual('BUTTON'); + expect(vm.$el.getAttribute('data-clipboard-text')).toEqual('copy me'); + expect(vm.$el.querySelector('i').className).toEqual('fa fa-clipboard'); + }); + + it('should have a tooltip with default values', () => { + expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value into Clipboard!'); + expect(vm.$el.getAttribute('data-placement')).toEqual('top'); + expect(vm.$el.getAttribute('data-container')).toEqual(null); + }); +}); diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js new file mode 100644 index 00000000000..a33ab689dd1 --- /dev/null +++ b/spec/javascripts/vue_shared/components/expand_button_spec.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import expandButton from '~/vue_shared/components/expand_button.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('expand button', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(expandButton); + vm = mountComponent(Component, { + slots: { + expanded: '<p>Expanded!</p>', + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a collpased button', () => { + expect(vm.$el.textContent.trim()).toEqual('...'); + }); + + it('hides expander on click', (done) => { + vm.$el.querySelector('button').click(); + vm.$nextTick(() => { + expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;'); + done(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index b4553acb341..b378a0bd896 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import headerCi from '~/vue_shared/components/header_ci_component.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('Header CI Component', () => { let HeaderCi; @@ -8,7 +9,6 @@ describe('Header CI Component', () => { beforeEach(() => { HeaderCi = Vue.extend(headerCi); - props = { status: { group: 'failed', @@ -45,54 +45,65 @@ describe('Header CI Component', () => { ], hasSidebarButton: true, }; - - vm = new HeaderCi({ - propsData: props, - }).$mount(); }); afterEach(() => { vm.$destroy(); }); - it('should render status badge', () => { - expect(vm.$el.querySelector('.ci-failed')).toBeDefined(); - expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined(); - expect( - vm.$el.querySelector('.ci-failed').getAttribute('href'), - ).toEqual(props.status.details_path); - }); + describe('render', () => { + beforeEach(() => { + vm = mountComponent(HeaderCi, props); + }); - it('should render item name and id', () => { - expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123'); - }); + it('should render status badge', () => { + expect(vm.$el.querySelector('.ci-failed')).toBeDefined(); + expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined(); + expect( + vm.$el.querySelector('.ci-failed').getAttribute('href'), + ).toEqual(props.status.details_path); + }); - it('should render timeago date', () => { - expect(vm.$el.querySelector('time')).toBeDefined(); - }); + it('should render item name and id', () => { + expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123'); + }); - it('should render user icon and name', () => { - expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name); - }); + it('should render timeago date', () => { + expect(vm.$el.querySelector('time')).toBeDefined(); + }); - it('should render provided actions', () => { - expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON'); - expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label); - expect(vm.$el.querySelector('.link').tagName).toEqual('A'); - expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); - expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path); - }); + it('should render user icon and name', () => { + expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name); + }); + + it('should render provided actions', () => { + expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON'); + expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label); + expect(vm.$el.querySelector('.link').tagName).toEqual('A'); + expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); + expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path); + }); - it('should show loading icon', (done) => { - vm.actions[0].isLoading = true; + it('should show loading icon', (done) => { + vm.actions[0].isLoading = true; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy(); - done(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy(); + done(); + }); + }); + + it('should render sidebar toggle button', () => { + expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined(); }); }); - it('should render sidebar toggle button', () => { - expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined(); + describe('shouldRenderTriggeredLabel', () => { + it('should rendered created keyword when the shouldRenderTriggeredLabel is false', () => { + vm = mountComponent(HeaderCi, { ...props, shouldRenderTriggeredLabel: false }); + + expect(vm.$el.textContent).toContain('created'); + expect(vm.$el.textContent).not.toContain('triggered'); + }); }); }); diff --git a/spec/javascripts/vue_shared/components/loading_icon_spec.js b/spec/javascripts/vue_shared/components/loading_icon_spec.js index 1baf3537741..5cd3466f501 100644 --- a/spec/javascripts/vue_shared/components/loading_icon_spec.js +++ b/spec/javascripts/vue_shared/components/loading_icon_spec.js @@ -16,7 +16,8 @@ describe('Loading Icon Component', () => { ).toEqual('fa fa-spin fa-spinner fa-1x'); expect(component.$el.tagName).toEqual('DIV'); - expect(component.$el.classList.contains('text-center')).toEqual(true); + expect(component.$el.classList).toContain('text-center'); + expect(component.$el.classList).toContain('loading-container'); }); it('should render accessibility attributes', () => { diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 24209be83fe..5f980bbf36c 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -12,14 +12,14 @@ describe('Markdown field component', () => { beforeEach((done) => { vm = new Vue({ + components: { + fieldComponent, + }, data() { return { text: 'testing\n123', }; }, - components: { - fieldComponent, - }, template: ` <field-component markdown-preview-path="/preview" diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js index 721f4044659..a5f9c75be4e 100644 --- a/spec/javascripts/vue_shared/components/modal_spec.js +++ b/spec/javascripts/vue_shared/components/modal_spec.js @@ -2,11 +2,65 @@ import Vue from 'vue'; import modal from '~/vue_shared/components/modal.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; +const modalComponent = Vue.extend(modal); + describe('Modal', () => { - it('does not render a primary button if no primaryButtonLabel', () => { - const modalComponent = Vue.extend(modal); - const vm = mountComponent(modalComponent); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('props', () => { + describe('without primaryButtonLabel', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { + primaryButtonLabel: null, + }); + }); + + it('does not render a primary button', () => { + expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + }); + }); + + describe('with id', () => { + describe('does not render a primary button', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { + id: 'my-modal', + }); + }); + + it('assigns the id to the modal', () => { + expect(vm.$el.querySelector('#my-modal.modal')).not.toBeNull(); + }); + + it('does not show the modal immediately', () => { + expect(vm.$el.querySelector('#my-modal.modal')).not.toHaveClass('show'); + }); + + it('does not show a backdrop', () => { + expect(vm.$el.querySelector('modal-backdrop')).toBeNull(); + }); + }); + }); + + it('works with data-toggle="modal"', (done) => { + setFixtures(` + <button id="modal-button" data-toggle="modal" data-target="#my-modal"></button> + <div id="modal-container"></div> + `); + + const modalContainer = document.getElementById('modal-container'); + const modalButton = document.getElementById('modal-button'); + vm = mountComponent(modalComponent, { + id: 'my-modal', + }, modalContainer); + const modalElement = vm.$el.querySelector('#my-modal'); + $(modalElement).on('shown.bs.modal', () => done()); - expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + modalButton.click(); + }); }); }); 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 index 20363e78094..2de108da2ac 100644 --- 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 @@ -21,22 +21,21 @@ describe('collapsedGroupedDatePicker', () => { }); }); - it('toggleCollapse events', () => { - const toggleCollapse = jasmine.createSpy(); - + describe('toggleCollapse events', () => { beforeEach((done) => { + spyOn(vm, 'toggleSidebar'); 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(); + expect(vm.toggleSidebar).toHaveBeenCalled(); }); it('should emit when collapsed-calendar-icon is clicked', () => { vm.$el.querySelector('.sidebar-collapsed-icon').click(); - expect(toggleCollapse).toHaveBeenCalled(); + expect(vm.toggleSidebar).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js new file mode 100644 index 00000000000..6940b04573e --- /dev/null +++ b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js @@ -0,0 +1,77 @@ +import Vue from 'vue'; + +import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (config) => { + const Component = Vue.extend(stackedProgressBarComponent); + const defaultConfig = Object.assign({}, { + successLabel: 'Synced', + failureLabel: 'Failed', + neutralLabel: 'Out of sync', + successCount: 10, + failureCount: 5, + totalCount: 20, + }, config); + + return mountComponent(Component, defaultConfig); +}; + +describe('StackedProgressBarComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('neutralCount', () => { + it('returns neutralCount based on totalCount, successCount and failureCount', () => { + expect(vm.neutralCount).toBe(5); // 20 - 10 - 5 + }); + }); + }); + + describe('methods', () => { + describe('getPercent', () => { + it('returns percentage from provided count based on `totalCount`', () => { + expect(vm.getPercent(10)).toBe(50); + }); + }); + + describe('barStyle', () => { + it('returns style string based on percentage provided', () => { + expect(vm.barStyle(50)).toBe('width: 50%;'); + }); + }); + + describe('getTooltip', () => { + it('returns label string based on label and count provided', () => { + expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10'); + }); + }); + }); + + describe('template', () => { + it('renders container element', () => { + expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy(); + }); + + it('renders empty state when count is unavailable', () => { + const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 }); + expect(vmX.$el.querySelectorAll('.status-unavailable').length).not.toBe(0); + vmX.$destroy(); + }); + + it('renders bar elements when count is available', () => { + expect(vm.$el.querySelectorAll('.status-green').length).not.toBe(0); + expect(vm.$el.querySelectorAll('.status-neutral').length).not.toBe(0); + expect(vm.$el.querySelectorAll('.status-red').length).not.toBe(0); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index 1465ef5855f..c63f15e5880 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -32,7 +32,7 @@ describe('Pagination component', () => { change: spy, }); - expect(component.$el.innerHTML).not.toBeDefined(); + expect(component.$el.childNodes.length).toEqual(0); }); describe('prev button', () => { @@ -72,7 +72,6 @@ describe('Pagination component', () => { }); component.$el.querySelector('.js-previous-button a').click(); - expect(spy).toHaveBeenCalledWith(1); }); }); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js index 8450ad9dbcb..adf80d0c2bb 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 45a0bb0650f..8edba1f47a3 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,4 +1,4 @@ -/* global Mousetrap */ +import Mousetrap from 'mousetrap'; import Dropzone from 'dropzone'; import ZenMode from '~/zen_mode'; |