diff options
| author | Achilleas Pipinellis <axil@gitlab.com> | 2019-08-20 20:22:43 +0200 |
|---|---|---|
| committer | Achilleas Pipinellis <axil@gitlab.com> | 2019-08-20 20:22:43 +0200 |
| commit | e61308ce1d82e12e5087371469baea4a452875d1 (patch) | |
| tree | 62551a3ae4eab75e5af7e3b35358c07a51b2132f /spec/javascripts | |
| parent | 4f323bb62fbe71a4352de25cab141f361a3fe1a6 (diff) | |
| parent | 2989ed078c1d45b0959dcecb1bc3c8f4740a3c0d (diff) | |
| download | gitlab-ce-docs-patch-71.tar.gz | |
Merge branch 'master' into docs-patch-71docs-patch-71
Diffstat (limited to 'spec/javascripts')
179 files changed, 3209 insertions, 4521 deletions
diff --git a/spec/javascripts/badges/components/badge_list_spec.js b/spec/javascripts/badges/components/badge_list_spec.js index 2f72c9ed89d..2fa807657de 100644 --- a/spec/javascripts/badges/components/badge_list_spec.js +++ b/spec/javascripts/badges/components/badge_list_spec.js @@ -60,7 +60,7 @@ describe('BadgeList component', () => { Vue.nextTick() .then(() => { - const loadingIcon = vm.$el.querySelector('.spinner'); + const loadingIcon = vm.$el.querySelector('.gl-spinner'); expect(loadingIcon).toBeVisible(); }) diff --git a/spec/javascripts/badges/components/badge_spec.js b/spec/javascripts/badges/components/badge_spec.js index 4e4d1ae2e99..c82a03a628a 100644 --- a/spec/javascripts/badges/components/badge_spec.js +++ b/spec/javascripts/badges/components/badge_spec.js @@ -15,7 +15,7 @@ describe('Badge component', () => { const buttons = vm.$el.querySelectorAll('button'); return { badgeImage: vm.$el.querySelector('img.project-badge'), - loadingIcon: vm.$el.querySelector('.spinner'), + loadingIcon: vm.$el.querySelector('.gl-spinner'), reloadButton: buttons[buttons.length - 1], }; }; diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js index 8e2a947b0dd..b64859ec32a 100644 --- a/spec/javascripts/boards/board_blank_state_spec.js +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -1,7 +1,6 @@ import Vue from 'vue'; import boardsStore from '~/boards/stores/boards_store'; import BoardBlankState from '~/boards/components/board_blank_state.vue'; -import { mockBoardService } from './mock_data'; describe('Boards blank state', () => { let vm; @@ -11,9 +10,10 @@ describe('Boards blank state', () => { const Comp = Vue.extend(BoardBlankState); boardsStore.create(); - gl.boardService = mockBoardService(); - spyOn(gl.boardService, 'generateDefaultLists').and.callFake( + spyOn(boardsStore, 'addList').and.stub(); + spyOn(boardsStore, 'removeList').and.stub(); + spyOn(boardsStore, 'generateDefaultLists').and.callFake( () => new Promise((resolve, reject) => { if (fail) { @@ -71,9 +71,14 @@ describe('Boards blank state', () => { vm.$el.querySelector('.btn-success').click(); setTimeout(() => { - expect(boardsStore.state.lists.length).toBe(2); - expect(boardsStore.state.lists[0].title).toEqual('To Do'); - expect(boardsStore.state.lists[1].title).toEqual('Doing'); + expect(boardsStore.addList).toHaveBeenCalledTimes(2); + expect(boardsStore.addList).toHaveBeenCalledWith( + jasmine.objectContaining({ title: 'To Do' }), + ); + + expect(boardsStore.addList).toHaveBeenCalledWith( + jasmine.objectContaining({ title: 'Doing' }), + ); done(); }); @@ -86,7 +91,7 @@ describe('Boards blank state', () => { setTimeout(() => { expect(boardsStore.welcomeIsHidden()).toBeFalsy(); - expect(boardsStore.state.lists.length).toBe(1); + expect(boardsStore.removeList).toHaveBeenCalledWith(undefined, 'label'); done(); }); diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 9c9b435d7fd..6774a46ed58 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -148,7 +148,7 @@ describe('Board list component', () => { component.list.loadingMore = true; Vue.nextTick(() => { - expect(component.$el.querySelector('.board-list-count .spinner')).not.toBeNull(); + expect(component.$el.querySelector('.board-list-count .gl-spinner')).not.toBeNull(); done(); }); diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index e81115e10c9..36bd7ada4f0 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -355,4 +355,27 @@ describe('Store', () => { expect(boardsStore.moving.list).toEqual(dummyList); }); }); + + describe('setTimeTrackingLimitToHours', () => { + it('sets the timeTracking.LimitToHours option', () => { + boardsStore.timeTracking.limitToHours = false; + + boardsStore.setTimeTrackingLimitToHours('true'); + + expect(boardsStore.timeTracking.limitToHours).toEqual(true); + }); + }); + + describe('setCurrentBoard', () => { + const dummyBoard = 'hoverboard'; + + it('sets the current board', () => { + const { state } = boardsStore; + state.currentBoard = null; + + boardsStore.setCurrentBoard(dummyBoard); + + expect(state.currentBoard).toEqual(dummyBoard); + }); + }); }); diff --git a/spec/javascripts/boards/components/board_form_spec.js b/spec/javascripts/boards/components/board_form_spec.js new file mode 100644 index 00000000000..e9014156a98 --- /dev/null +++ b/spec/javascripts/boards/components/board_form_spec.js @@ -0,0 +1,56 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import boardsStore from '~/boards/stores/boards_store'; +import boardForm from '~/boards/components/board_form.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('board_form.vue', () => { + const props = { + canAdminBoard: false, + labelsPath: `${gl.TEST_HOST}/labels/path`, + milestonePath: `${gl.TEST_HOST}/milestone/path`, + }; + let vm; + + beforeEach(() => { + spyOn($, 'ajax'); + boardsStore.state.currentPage = 'edit'; + const Component = Vue.extend(boardForm); + vm = mountComponent(Component, props); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + describe('cancel', () => { + it('resets currentPage', done => { + vm.cancel(); + + Vue.nextTick() + .then(() => { + expect(boardsStore.state.currentPage).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('buttons', () => { + it('cancel button triggers cancel()', done => { + spyOn(vm, 'cancel'); + + Vue.nextTick() + .then(() => { + const cancelButton = vm.$el.querySelector('button[data-dismiss="modal"]'); + cancelButton.click(); + + expect(vm.cancel).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index d08ee41802b..683783334c6 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -1,7 +1,6 @@ import Vue from 'vue'; -import '~/boards/services/board_service'; import Board from '~/boards/components/board'; -import '~/boards/models/list'; +import List from '~/boards/models/list'; import { mockBoardService } from '../mock_data'; describe('Board component', () => { @@ -27,7 +26,6 @@ describe('Board component', () => { disabled: false, issueLinkBase: '/', rootPath: '/', - // eslint-disable-next-line no-undef list: new List({ id: 1, position: 0, @@ -53,57 +51,62 @@ describe('Board component', () => { expect(vm.$el.classList.contains('is-expandable')).toBe(true); }); - it('board is expandable when list type is closed', done => { - vm.list.type = 'closed'; - - Vue.nextTick(() => { - expect(vm.$el.classList.contains('is-expandable')).toBe(true); - - done(); - }); + it('board is expandable when list type is closed', () => { + expect(new List({ id: 1, list_type: 'closed' }).isExpandable).toBe(true); }); - it('board is not expandable when list type is label', done => { - vm.list.type = 'label'; - vm.list.isExpandable = false; - - Vue.nextTick(() => { - expect(vm.$el.classList.contains('is-expandable')).toBe(false); + it('board is expandable when list type is label', () => { + expect(new List({ id: 1, list_type: 'closed' }).isExpandable).toBe(true); + }); - done(); - }); + it('board is not expandable when list type is blank', () => { + expect(new List({ id: 1, list_type: 'blank' }).isExpandable).toBe(false); }); - it('collapses when clicking header', done => { + it('does not collapse when clicking header', done => { + vm.list.isExpanded = true; vm.$el.querySelector('.board-header').click(); Vue.nextTick(() => { - expect(vm.$el.classList.contains('is-collapsed')).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(false); done(); }); }); - it('created sets isExpanded to true from localStorage', done => { - vm.$el.querySelector('.board-header').click(); + it('collapses when clicking the collapse icon', done => { + vm.list.isExpanded = true; - return Vue.nextTick() + Vue.nextTick() + .then(() => { + vm.$el.querySelector('.board-title-caret').click(); + }) .then(() => { expect(vm.$el.classList.contains('is-collapsed')).toBe(true); + done(); + }) + .catch(done.fail); + }); - // call created manually - vm.$options.created[0].call(vm); + it('expands when clicking the expand icon', done => { + vm.list.isExpanded = false; - return Vue.nextTick(); + Vue.nextTick() + .then(() => { + vm.$el.querySelector('.board-title-caret').click(); }) .then(() => { - expect(vm.$el.classList.contains('is-collapsed')).toBe(true); - + expect(vm.$el.classList.contains('is-collapsed')).toBe(false); done(); }) .catch(done.fail); }); + it('is expanded when created', () => { + expect(vm.list.isExpanded).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(false); + }); + it('does render add issue button', () => { expect(vm.$el.querySelector('.issue-count-badge-add-button')).not.toBeNull(); }); diff --git a/spec/javascripts/boards/components/boards_selector_spec.js b/spec/javascripts/boards/components/boards_selector_spec.js new file mode 100644 index 00000000000..473cc0612ea --- /dev/null +++ b/spec/javascripts/boards/components/boards_selector_spec.js @@ -0,0 +1,206 @@ +import Vue from 'vue'; +import BoardService from '~/boards/services/board_service'; +import BoardsSelector from '~/boards/components/boards_selector.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'spec/test_constants'; +import boardsStore from '~/boards/stores/boards_store'; + +const throttleDuration = 1; + +function boardGenerator(n) { + return new Array(n).fill().map((board, id) => { + const name = `board${id}`; + + return { + id, + name, + }; + }); +} + +describe('BoardsSelector', () => { + let vm; + let allBoardsResponse; + let recentBoardsResponse; + let fillSearchBox; + const boards = boardGenerator(20); + const recentBoards = boardGenerator(5); + + beforeEach(done => { + setFixtures('<div class="js-boards-selector"></div>'); + window.gl = window.gl || {}; + + boardsStore.setEndpoints({ + boardsEndpoint: '', + recentBoardsEndpoint: '', + listsEndpoint: '', + bulkUpdatePath: '', + boardId: '', + }); + window.gl.boardService = new BoardService(); + + allBoardsResponse = Promise.resolve({ + data: boards, + }); + recentBoardsResponse = Promise.resolve({ + data: recentBoards, + }); + + spyOn(BoardService.prototype, 'allBoards').and.returnValue(allBoardsResponse); + spyOn(BoardService.prototype, 'recentBoards').and.returnValue(recentBoardsResponse); + + const Component = Vue.extend(BoardsSelector); + vm = mountComponent( + Component, + { + throttleDuration, + currentBoard: { + id: 1, + name: 'Development', + milestone_id: null, + weight: null, + assignee_id: null, + labels: [], + }, + milestonePath: `${TEST_HOST}/milestone/path`, + boardBaseUrl: `${TEST_HOST}/board/base/url`, + hasMissingBoards: false, + canAdminBoard: true, + multipleIssueBoardsAvailable: true, + labelsPath: `${TEST_HOST}/labels/path`, + projectId: 42, + groupId: 19, + scopedIssueBoardFeatureEnabled: true, + weights: [], + }, + document.querySelector('.js-boards-selector'), + ); + + // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time + vm.$children[0].$emit('show'); + + Promise.all([allBoardsResponse, recentBoardsResponse]) + .then(() => vm.$nextTick()) + .then(done) + .catch(done.fail); + + fillSearchBox = filterTerm => { + const { searchBox } = vm.$refs; + const searchBoxInput = searchBox.$el.querySelector('input'); + searchBoxInput.value = filterTerm; + searchBoxInput.dispatchEvent(new Event('input')); + }; + }); + + afterEach(() => { + vm.$destroy(); + window.gl.boardService = undefined; + }); + + describe('filtering', () => { + it('shows all boards without filtering', done => { + vm.$nextTick() + .then(() => { + const dropdownItem = vm.$el.querySelectorAll('.js-dropdown-item'); + + expect(dropdownItem.length).toBe(boards.length + recentBoards.length); + }) + .then(done) + .catch(done.fail); + }); + + it('shows only matching boards when filtering', done => { + const filterTerm = 'board1'; + const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length; + + fillSearchBox(filterTerm); + + vm.$nextTick() + .then(() => { + const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item'); + + expect(dropdownItems.length).toBe(expectedCount); + }) + .then(done) + .catch(done.fail); + }); + + it('shows message if there are no matching boards', done => { + fillSearchBox('does not exist'); + + vm.$nextTick() + .then(() => { + const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item'); + + expect(dropdownItems.length).toBe(0); + expect(vm.$el).toContainText('No matching boards found'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('recent boards section', () => { + it('shows only when boards are greater than 10', done => { + vm.$nextTick() + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + + const expectedCount = 2; // Recent + All + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + + it('does not show when boards are less than 10', done => { + spyOn(vm, 'initScrollFade'); + spyOn(vm, 'setScrollFade'); + + vm.$nextTick() + .then(() => { + vm.boards = vm.boards.slice(0, 5); + }) + .then(vm.$nextTick) + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + const expectedCount = 0; + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + + it('does not show when recentBoards api returns empty array', done => { + vm.$nextTick() + .then(() => { + vm.recentBoards = []; + }) + .then(vm.$nextTick) + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + const expectedCount = 0; + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + + it('does not show when search is active', done => { + fillSearchBox('Random string'); + + vm.$nextTick() + .then(() => { + const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); + const expectedCount = 0; + + expect(expectedCount).toBe(headerEls.length); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/boards/components/issue_time_estimate_spec.js b/spec/javascripts/boards/components/issue_time_estimate_spec.js index ba65d3287da..de48e3f6091 100644 --- a/spec/javascripts/boards/components/issue_time_estimate_spec.js +++ b/spec/javascripts/boards/components/issue_time_estimate_spec.js @@ -1,40 +1,70 @@ import Vue from 'vue'; import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue'; +import boardsStore from '~/boards/stores/boards_store'; import mountComponent from '../../helpers/vue_mount_component_helper'; -describe('Issue Tine Estimate component', () => { +describe('Issue Time Estimate component', () => { let vm; beforeEach(() => { - const Component = Vue.extend(IssueTimeEstimate); - vm = mountComponent(Component, { - estimate: 374460, - }); + boardsStore.create(); }); afterEach(() => { vm.$destroy(); }); - it('renders the correct time estimate', () => { - expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m'); - }); + describe('when limitToHours is false', () => { + beforeEach(() => { + boardsStore.timeTracking.limitToHours = false; + + const Component = Vue.extend(IssueTimeEstimate); + vm = mountComponent(Component, { + estimate: 374460, + }); + }); + + it('renders the correct time estimate', () => { + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m'); + }); + + it('renders expanded time estimate in tooltip', () => { + expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain( + '2 weeks 3 days 1 minute', + ); + }); + + it('prevents tooltip xss', done => { + const alertSpy = spyOn(window, 'alert'); + vm.estimate = 'Foo <script>alert("XSS")</script>'; - it('renders expanded time estimate in tooltip', () => { - expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain( - '2 weeks 3 days 1 minute', - ); + vm.$nextTick(() => { + expect(alertSpy).not.toHaveBeenCalled(); + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m'); + expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m'); + done(); + }); + }); }); - it('prevents tooltip xss', done => { - const alertSpy = spyOn(window, 'alert'); - vm.estimate = 'Foo <script>alert("XSS")</script>'; + describe('when limitToHours is true', () => { + beforeEach(() => { + boardsStore.timeTracking.limitToHours = true; + + const Component = Vue.extend(IssueTimeEstimate); + vm = mountComponent(Component, { + estimate: 374460, + }); + }); + + it('renders the correct time estimate', () => { + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('104h 1m'); + }); - vm.$nextTick(() => { - expect(alertSpy).not.toHaveBeenCalled(); - expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m'); - expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m'); - done(); + it('renders expanded time estimate in tooltip', () => { + expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain( + '104 hours 1 minute', + ); }); }); }); diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 9854cf49e97..ea22ae5c4e7 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,4 +1,5 @@ import BoardService from '~/boards/services/board_service'; +import boardsStore from '~/boards/stores/boards_store'; export const boardObj = { id: 1, @@ -76,12 +77,14 @@ export const mockBoardService = (opts = {}) => { const bulkUpdatePath = opts.bulkUpdatePath || ''; const boardId = opts.boardId || '1'; - return new BoardService({ + boardsStore.setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, }); + + return new BoardService(); }; export const mockAssigneesList = [ diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js index e6a969bd855..b2fe315f6c6 100644 --- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js @@ -224,7 +224,7 @@ describe('AjaxFormVariableList', () => { describe('maskableRegex', () => { it('takes in the regex provided by the data attribute', () => { - expect(container.dataset.maskableRegex).toBe('^[a-zA-Z0-9_+=/-]{8,}$'); + expect(container.dataset.maskableRegex).toBe('^[a-zA-Z0-9_+=/@:-]{8,}$'); expect(ajaxVariableList.maskableRegex).toBe(container.dataset.maskableRegex); }); }); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index 064113e879a..c8d6f789ed0 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -162,7 +162,7 @@ describe('VariableList', () => { }); it('has a regex provided via a data attribute', () => { - expect($wrapper.attr('data-maskable-regex')).toBe('^[a-zA-Z0-9_+=/-]{8,}$'); + expect($wrapper.attr('data-maskable-regex')).toBe('^[a-zA-Z0-9_+=/@:-]{8,}$'); }); it('allows values that are 8 characters long', done => { diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js index bb90e53e525..f75d63c8f57 100644 --- a/spec/javascripts/collapsed_sidebar_todo_spec.js +++ b/spec/javascripts/collapsed_sidebar_todo_spec.js @@ -58,7 +58,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { it('sets default tooltip title', () => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('title'), - ).toBe('Add todo'); + ).toBe('Add a To Do'); }); it('toggle todo state', done => { @@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { setTimeout(() => { expect( document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); done(); }); @@ -99,7 +99,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('data-original-title'), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); done(); }); @@ -124,13 +124,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => { expect( document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Add todo'); + ).toBe('Add a To Do'); }) .then(done) .catch(done.fail); }); - it('updates aria-label to mark todo as done', done => { + it('updates aria-label to Mark as done', done => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); setTimeout(() => { @@ -138,7 +138,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('aria-label'), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); done(); }); @@ -153,7 +153,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('aria-label'), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); }) @@ -163,7 +163,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('aria-label'), - ).toBe('Add todo'); + ).toBe('Add a To Do'); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/create_merge_request_dropdown_spec.js b/spec/javascripts/create_merge_request_dropdown_spec.js deleted file mode 100644 index 00fe3f451f5..00000000000 --- a/spec/javascripts/create_merge_request_dropdown_spec.js +++ /dev/null @@ -1,68 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; -import MockAdapter from 'axios-mock-adapter'; -import CreateMergeRequestDropdown from '~/create_merge_request_dropdown'; -import { TEST_HOST } from 'spec/test_constants'; - -describe('CreateMergeRequestDropdown', () => { - let axiosMock; - let dropdown; - - beforeEach(() => { - axiosMock = new MockAdapter(axios); - - setFixtures(` - <div id="dummy-wrapper-element"> - <div class="available"></div> - <div class="unavailable"> - <div class="fa"></div> - <div class="text"></div> - </div> - <div class="js-ref"></div> - <div class="js-create-merge-request"></div> - <div class="js-create-target"></div> - <div class="js-dropdown-toggle"></div> - </div> - `); - - const dummyElement = document.getElementById('dummy-wrapper-element'); - dropdown = new CreateMergeRequestDropdown(dummyElement); - dropdown.refsPath = `${TEST_HOST}/dummy/refs?search=`; - }); - - afterEach(() => { - axiosMock.restore(); - }); - - describe('getRef', () => { - it('escapes branch names correctly', done => { - const endpoint = `${dropdown.refsPath}contains%23hash`; - spyOn(axios, 'get').and.callThrough(); - axiosMock.onGet(endpoint).replyOnce({}); - - dropdown - .getRef('contains#hash') - .then(() => { - expect(axios.get).toHaveBeenCalledWith(endpoint); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('updateCreatePaths', () => { - it('escapes branch names correctly', () => { - dropdown.createBranchPath = `${TEST_HOST}/branches?branch_name=some-branch&issue=42`; - dropdown.createMrPath = `${TEST_HOST}/create_merge_request?branch_name=some-branch&ref=master`; - - dropdown.updateCreatePaths('branch', 'contains#hash'); - - expect(dropdown.createBranchPath).toBe( - `${TEST_HOST}/branches?branch_name=contains%23hash&issue=42`, - ); - - expect(dropdown.createMrPath).toBe( - `${TEST_HOST}/create_merge_request?branch_name=contains%23hash&ref=master`, - ); - }); - }); -}); diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index 1aabf3c2132..fdf8bcee756 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -37,6 +37,8 @@ describe('diffs/components/app', () => { projectPath: 'namespace/project', currentUser: {}, changesEmptyStateIllustration: '', + dismissEndpoint: '', + showSuggestPopover: true, ...props, }, store, diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index 77f8352047c..ef4bb470734 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import CompareVersionsComponent from '~/diffs/components/compare_versions.vue'; -import store from '~/mr_notes/stores'; +import { createStore } from '~/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffsMockData from '../mock_data/merge_request_diffs'; import getDiffWithCommit from '../mock_data/diff_with_commit'; @@ -10,6 +10,8 @@ describe('CompareVersions', () => { const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 }; beforeEach(() => { + const store = createStore(); + store.state.diffs.addedLines = 10; store.state.diffs.removedLines = 20; store.state.diffs.diffFiles.push('test'); diff --git a/spec/javascripts/diffs/components/diff_expansion_cell_spec.js b/spec/javascripts/diffs/components/diff_expansion_cell_spec.js new file mode 100644 index 00000000000..63c50c09fce --- /dev/null +++ b/spec/javascripts/diffs/components/diff_expansion_cell_spec.js @@ -0,0 +1,64 @@ +import Vue from 'vue'; +import { createStore } from '~/mr_notes/stores'; +import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +const EXPAND_UP_CLASS = '.js-unfold'; +const EXPAND_DOWN_CLASS = '.js-unfold-down'; +const EXPAND_ALL_CLASS = '.js-unfold-all'; + +describe('DiffExpansionCell', () => { + const matchLine = diffFileMockData.highlighted_diff_lines[5]; + + const createComponent = (options = {}) => { + const cmp = Vue.extend(DiffExpansionCell); + const defaults = { + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + line: matchLine, + isTop: false, + isBottom: false, + }; + const props = Object.assign({}, defaults, options); + + return createComponentWithStore(cmp, createStore(), props).$mount(); + }; + + describe('top row', () => { + it('should have "expand up" and "show all" option', () => { + const vm = createComponent({ + isTop: true, + }); + const el = vm.$el; + + expect(el.querySelector(EXPAND_UP_CLASS)).not.toBe(null); + expect(el.querySelector(EXPAND_DOWN_CLASS)).toBe(null); + expect(el.querySelector(EXPAND_ALL_CLASS)).not.toBe(null); + }); + }); + + describe('middle row', () => { + it('should have "expand down", "show all", "expand up" option', () => { + const vm = createComponent(); + const el = vm.$el; + + expect(el.querySelector(EXPAND_UP_CLASS)).not.toBe(null); + expect(el.querySelector(EXPAND_DOWN_CLASS)).not.toBe(null); + expect(el.querySelector(EXPAND_ALL_CLASS)).not.toBe(null); + }); + }); + + describe('bottom row', () => { + it('should have "expand down" and "show all" option', () => { + const vm = createComponent({ + isBottom: true, + }); + const el = vm.$el; + + expect(el.querySelector(EXPAND_UP_CLASS)).toBe(null); + expect(el.querySelector(EXPAND_DOWN_CLASS)).not.toBe(null); + expect(el.querySelector(EXPAND_ALL_CLASS)).not.toBe(null); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 596a1ba5ad2..d4280d3ec2c 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -521,7 +521,7 @@ describe('diff_file_header', () => { }); describe('with discussions', () => { - it('dispatches toggleFileDiscussions when user clicks on toggle discussions button', () => { + it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => { const propsCopy = Object.assign({}, props); propsCopy.diffFile.submodule = false; propsCopy.diffFile.blob = { @@ -552,11 +552,11 @@ describe('diff_file_header', () => { }), }); - spyOn(vm, 'toggleFileDiscussions'); + spyOn(vm, 'toggleFileDiscussionWrappers'); vm.$el.querySelector('.js-btn-vue-toggle-comments').click(); - expect(vm.toggleFileDiscussions).toHaveBeenCalled(); + expect(vm.toggleFileDiscussionWrappers).toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index ef4589ada48..3ca2d1dc934 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import DiffFileComponent from '~/diffs/components/diff_file.vue'; import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; -import store from 'ee_else_ce/mr_notes/stores'; +import { createStore } from 'ee_else_ce/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; @@ -9,14 +9,18 @@ describe('DiffFile', () => { let vm; beforeEach(() => { - vm = createComponentWithStore(Vue.extend(DiffFileComponent), store, { + vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { file: JSON.parse(JSON.stringify(diffFileMockData)), canCurrentUserFork: false, }).$mount(); }); + afterEach(() => { + vm.$destroy(); + }); + describe('template', () => { - it('should render component with file header, file content components', () => { + it('should render component with file header, file content components', done => { const el = vm.$el; const { file_hash, file_path } = vm.file; @@ -30,9 +34,13 @@ describe('DiffFile', () => { vm.file.renderIt = true; - vm.$nextTick(() => { - expect(el.querySelectorAll('.line_content').length).toBeGreaterThan(5); - }); + vm.$nextTick() + .then(() => { + expect(el.querySelectorAll('.line_content').length).toBe(5); + expect(el.querySelectorAll('.js-line-expansion-content').length).toBe(1); + }) + .then(done) + .catch(done.fail); }); describe('collapsed', () => { diff --git a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js deleted file mode 100644 index cdd30919b09..00000000000 --- a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js +++ /dev/null @@ -1,146 +0,0 @@ -import Vue from 'vue'; -import DiffGutterAvatarsComponent from '~/diffs/components/diff_gutter_avatars.vue'; -import { COUNT_OF_AVATARS_IN_GUTTER } from '~/diffs/constants'; -import store from '~/mr_notes/stores'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import discussionsMockData from '../mock_data/diff_discussions'; - -describe('DiffGutterAvatars', () => { - let component; - const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; - - beforeEach(() => { - component = createComponentWithStore(Vue.extend(DiffGutterAvatarsComponent), store, { - discussions: getDiscussionsMockData(), - }).$mount(); - }); - - describe('computed', () => { - describe('discussionsExpanded', () => { - it('should return true when all discussions are expanded', () => { - expect(component.discussionsExpanded).toEqual(true); - }); - - it('should return false when all discussions are not expanded', () => { - component.discussions[0].expanded = false; - - expect(component.discussionsExpanded).toEqual(false); - }); - }); - - describe('allDiscussions', () => { - it('should return an array of notes', () => { - expect(component.allDiscussions).toEqual([...component.discussions[0].notes]); - }); - }); - - describe('notesInGutter', () => { - it('should return a subset of discussions to show in gutter', () => { - expect(component.notesInGutter.length).toEqual(COUNT_OF_AVATARS_IN_GUTTER); - expect(component.notesInGutter[0]).toEqual({ - note: component.discussions[0].notes[0].note, - author: component.discussions[0].notes[0].author, - }); - }); - }); - - describe('moreCount', () => { - it('should return count of remaining discussions from gutter', () => { - expect(component.moreCount).toEqual(2); - }); - }); - - describe('moreText', () => { - it('should return proper text if moreCount > 0', () => { - expect(component.moreText).toEqual('2 more comments'); - }); - - it('should return empty string if there is no discussion', () => { - component.discussions = []; - - expect(component.moreText).toEqual(''); - }); - }); - }); - - describe('methods', () => { - describe('getTooltipText', () => { - it('should return original comment if it is shorter than max length', () => { - const note = component.discussions[0].notes[0]; - - expect(component.getTooltipText(note)).toEqual('Administrator: comment 1'); - }); - - it('should return truncated version of comment', () => { - const note = component.discussions[0].notes[1]; - - expect(component.getTooltipText(note)).toEqual('Fatih Acet: comment 2 is r...'); - }); - }); - - describe('toggleDiscussions', () => { - it('should toggle all discussions', () => { - expect(component.discussions[0].expanded).toEqual(true); - - component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); - component.discussions = component.$store.state.notes.discussions; - component.toggleDiscussions(); - - expect(component.discussions[0].expanded).toEqual(false); - component.$store.dispatch('setInitialNotes', []); - }); - - it('forces expansion of all discussions', () => { - spyOn(component.$store, 'dispatch'); - - component.discussions[0].expanded = true; - component.discussions.push({ - ...component.discussions[0], - id: '123test', - expanded: false, - }); - - component.toggleDiscussions(); - - expect(component.$store.dispatch.calls.argsFor(0)).toEqual([ - 'toggleDiscussion', - { - discussionId: component.discussions[0].id, - forceExpanded: true, - }, - ]); - - expect(component.$store.dispatch.calls.argsFor(1)).toEqual([ - 'toggleDiscussion', - { - discussionId: component.discussions[1].id, - forceExpanded: true, - }, - ]); - }); - }); - }); - - describe('template', () => { - const buttonSelector = '.js-diff-comment-button'; - const svgSelector = `${buttonSelector} svg`; - const avatarSelector = '.js-diff-comment-avatar'; - const plusCountSelector = '.js-diff-comment-plus'; - - it('should have button to collapse discussions when the discussions expanded', () => { - expect(component.$el.querySelector(buttonSelector)).toBeDefined(); - expect(component.$el.querySelector(svgSelector)).toBeDefined(); - }); - - it('should have user avatars when discussions collapsed', () => { - component.discussions[0].expanded = false; - - Vue.nextTick(() => { - expect(component.$el.querySelector(buttonSelector)).toBeNull(); - expect(component.$el.querySelectorAll(avatarSelector).length).toEqual(4); - expect(component.$el.querySelector(plusCountSelector)).toBeDefined(); - expect(component.$el.querySelector(plusCountSelector).textContent).toEqual('+2'); - }); - }); - }); -}); diff --git a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js index 038db8eaa7c..6bb704658fb 100644 --- a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js +++ b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import DiffLineGutterContent from '~/diffs/components/diff_line_gutter_content.vue'; -import store from '~/mr_notes/stores'; +import { createStore } from '~/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import discussionsMockData from '../mock_data/diff_discussions'; import diffFileMockData from '../mock_data/diff_file'; @@ -23,7 +23,7 @@ describe('DiffLineGutterContent', () => { props.fileHash = getDiffFileMock().file_hash; props.contextLinesPath = '/context/lines/path'; - return createComponentWithStore(cmp, store, props).$mount(); + return createComponentWithStore(cmp, createStore(), props).$mount(); }; describe('computed', () => { @@ -61,7 +61,7 @@ describe('DiffLineGutterContent', () => { contextLinesPath: '/context/lines/path', }; props.line.discussions = [Object.assign({}, discussionsMockData)]; - const component = createComponentWithStore(cmp, store, props).$mount(); + const component = createComponentWithStore(cmp, createStore(), props).$mount(); expect(component.hasDiscussions).toEqual(true); expect(component.shouldShowAvatarsOnGutter).toEqual(true); @@ -70,15 +70,6 @@ describe('DiffLineGutterContent', () => { }); describe('template', () => { - it('should render three dots for context lines', () => { - const component = createComponent({ - isMatchLine: true, - }); - - expect(component.$el.querySelector('span').classList.contains('context-cell')).toEqual(true); - expect(component.$el.innerText).toEqual('...'); - }); - it('should render comment button', () => { const component = createComponent({ showCommentButton: true, diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js index b983dc35a57..237cfccfa29 100644 --- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js +++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue'; -import store from '~/mr_notes/stores'; +import { createStore } from '~/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; import { noteableDataMock } from '../../notes/mock_data'; @@ -15,7 +15,7 @@ describe('DiffLineNoteForm', () => { diffFile = getDiffFileMock(); diffLines = diffFile.highlighted_diff_lines; - component = createComponentWithStore(Vue.extend(DiffLineNoteForm), store, { + component = createComponentWithStore(Vue.extend(DiffLineNoteForm), createStore(), { diffFileHash: diffFile.file_hash, diffLines, line: diffLines[0], diff --git a/spec/javascripts/diffs/components/diff_table_cell_spec.js b/spec/javascripts/diffs/components/diff_table_cell_spec.js index 170e661beea..a5a042c577c 100644 --- a/spec/javascripts/diffs/components/diff_table_cell_spec.js +++ b/spec/javascripts/diffs/components/diff_table_cell_spec.js @@ -1,12 +1,12 @@ import Vue from 'vue'; -import store from '~/mr_notes/stores'; +import { createStore } from '~/mr_notes/stores'; import DiffTableCell from '~/diffs/components/diff_table_cell.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; describe('DiffTableCell', () => { const createComponent = options => - createComponentWithStore(Vue.extend(DiffTableCell), store, { + createComponentWithStore(Vue.extend(DiffTableCell), createStore(), { line: diffFileMockData.highlighted_diff_lines[0], fileHash: diffFileMockData.file_hash, contextLinesPath: 'contextLinesPath', diff --git a/spec/javascripts/diffs/components/inline_diff_expansion_row_spec.js b/spec/javascripts/diffs/components/inline_diff_expansion_row_spec.js new file mode 100644 index 00000000000..290b3d7c803 --- /dev/null +++ b/spec/javascripts/diffs/components/inline_diff_expansion_row_spec.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import { createStore } from '~/mr_notes/stores'; +import InlineDiffExpansionRow from '~/diffs/components/inline_diff_expansion_row.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('InlineDiffExpansionRow', () => { + const matchLine = diffFileMockData.highlighted_diff_lines[5]; + + const createComponent = (options = {}) => { + const cmp = Vue.extend(InlineDiffExpansionRow); + const defaults = { + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + line: matchLine, + isTop: false, + isBottom: false, + }; + const props = Object.assign({}, defaults, options); + + return createComponentWithStore(cmp, createStore(), props).$mount(); + }; + + describe('template', () => { + it('should render expansion row for match lines', () => { + const vm = createComponent(); + + expect(vm.$el.classList.contains('line_expansion')).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/inline_diff_table_row_spec.js b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js index 97926f6625e..0ddffe926d9 100644 --- a/spec/javascripts/diffs/components/inline_diff_table_row_spec.js +++ b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import store from '~/mr_notes/stores'; +import { createStore } from '~/mr_notes/stores'; import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; @@ -9,7 +9,7 @@ describe('InlineDiffTableRow', () => { const thisLine = diffFileMockData.highlighted_diff_lines[0]; beforeEach(() => { - vm = createComponentWithStore(Vue.extend(InlineDiffTableRow), store, { + vm = createComponentWithStore(Vue.extend(InlineDiffTableRow), createStore(), { line: thisLine, fileHash: diffFileMockData.file_hash, contextLinesPath: 'contextLinesPath', diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js index 4452106580a..486d9629e26 100644 --- a/spec/javascripts/diffs/components/inline_diff_view_spec.js +++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import '~/behaviors/markdown/render_gfm'; import InlineDiffView from '~/diffs/components/inline_diff_view.vue'; -import store from 'ee_else_ce/mr_notes/stores'; +import { createStore } from 'ee_else_ce/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; import discussionsMockData from '../mock_data/diff_discussions'; @@ -10,10 +10,13 @@ describe('InlineDiffView', () => { let component; const getDiffFileMock = () => Object.assign({}, diffFileMockData); const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + const notesLength = getDiscussionsMockData()[0].notes.length; beforeEach(done => { const diffFile = getDiffFileMock(); + const store = createStore(); + store.dispatch('diffs/setInlineDiffViewType'); component = createComponentWithStore(Vue.extend(InlineDiffView), store, { diffFile, @@ -27,19 +30,20 @@ describe('InlineDiffView', () => { it('should have rendered diff lines', () => { const el = component.$el; - expect(el.querySelectorAll('tr.line_holder').length).toEqual(6); + expect(el.querySelectorAll('tr.line_holder').length).toEqual(5); expect(el.querySelectorAll('tr.line_holder.new').length).toEqual(2); - expect(el.querySelectorAll('tr.line_holder.match').length).toEqual(1); + expect(el.querySelectorAll('tr.line_expansion.match').length).toEqual(1); expect(el.textContent.indexOf('Bad dates')).toBeGreaterThan(-1); }); it('should render discussions', done => { const el = component.$el; component.diffLines[1].discussions = getDiscussionsMockData(); + component.diffLines[1].discussionsExpanded = true; Vue.nextTick(() => { expect(el.querySelectorAll('.notes_holder').length).toEqual(1); - expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5); + expect(el.querySelectorAll('.notes_holder .note').length).toEqual(notesLength + 1); expect(el.innerText.indexOf('comment 5')).toBeGreaterThan(-1); component.$store.dispatch('setInitialNotes', []); diff --git a/spec/javascripts/diffs/components/parallel_diff_expansion_row_spec.js b/spec/javascripts/diffs/components/parallel_diff_expansion_row_spec.js new file mode 100644 index 00000000000..a766ebb5efb --- /dev/null +++ b/spec/javascripts/diffs/components/parallel_diff_expansion_row_spec.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import { createStore } from '~/mr_notes/stores'; +import ParallelDiffExpansionRow from '~/diffs/components/parallel_diff_expansion_row.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('ParallelDiffExpansionRow', () => { + const matchLine = diffFileMockData.highlighted_diff_lines[5]; + + const createComponent = (options = {}) => { + const cmp = Vue.extend(ParallelDiffExpansionRow); + const defaults = { + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + line: matchLine, + isTop: false, + isBottom: false, + }; + const props = Object.assign({}, defaults, options); + + return createComponentWithStore(cmp, createStore(), props).$mount(); + }; + + describe('template', () => { + it('should render expansion row for match lines', () => { + const vm = createComponent(); + + expect(vm.$el.classList.contains('line_expansion')).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/parallel_diff_view_spec.js b/spec/javascripts/diffs/components/parallel_diff_view_spec.js index 236bda96145..191313bf487 100644 --- a/spec/javascripts/diffs/components/parallel_diff_view_spec.js +++ b/spec/javascripts/diffs/components/parallel_diff_view_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue'; -import store from 'ee_else_ce/mr_notes/stores'; +import { createStore } from 'ee_else_ce/mr_notes/stores'; import * as constants from '~/diffs/constants'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; @@ -12,7 +12,7 @@ describe('ParallelDiffView', () => { beforeEach(() => { const diffFile = getDiffFileMock(); - component = createComponentWithStore(Vue.extend(ParallelDiffView), store, { + component = createComponentWithStore(Vue.extend(ParallelDiffView), createStore(), { diffFile, diffLines: diffFile.parallel_diff_lines, }).$mount(); diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js index 27428197c1c..531686efff1 100644 --- a/spec/javascripts/diffs/mock_data/diff_file.js +++ b/spec/javascripts/diffs/mock_data/diff_file.js @@ -1,3 +1,5 @@ +// Copied to ee/spec/frontend/diffs/mock_data/diff_file.js + export default { submodule: false, submodule_link: null, diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index f129fbb57a3..5806cb47034 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -37,6 +37,7 @@ import actions, { toggleFullDiff, setFileCollapsed, setExpandedDiffLines, + setSuggestPopoverDismissed, } from '~/diffs/store/actions'; import eventHub from '~/notes/event_hub'; import * as types from '~/diffs/store/mutation_types'; @@ -68,12 +69,19 @@ describe('DiffsStoreActions', () => { it('should set given endpoint and project path', done => { const endpoint = '/diffs/set/endpoint'; const projectPath = '/root/project'; + const dismissEndpoint = '/-/user_callouts'; + const showSuggestPopover = false; testAction( setBaseConfig, - { endpoint, projectPath }, - { endpoint: '', projectPath: '' }, - [{ type: types.SET_BASE_CONFIG, payload: { endpoint, projectPath } }], + { endpoint, projectPath, dismissEndpoint, showSuggestPopover }, + { endpoint: '', projectPath: '', dismissEndpoint: '', showSuggestPopover: true }, + [ + { + type: types.SET_BASE_CONFIG, + payload: { endpoint, projectPath, dismissEndpoint, showSuggestPopover }, + }, + ], [], done, ); @@ -198,6 +206,7 @@ describe('DiffsStoreActions', () => { position_type: 'text', }, }, + hash: 'diff-content-1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', }, }, ], @@ -371,7 +380,9 @@ describe('DiffsStoreActions', () => { const params = { since: 6, to: 26 }; const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; const fileHash = 'ff9200'; - const options = { endpoint, params, lineNumbers, fileHash }; + const isExpandDown = false; + const nextLineNumbers = {}; + const options = { endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers }; const mock = new MockAdapter(axios); const contextLines = { contextLines: [{ lineCode: 6 }] }; mock.onGet(endpoint).reply(200, contextLines); @@ -383,7 +394,7 @@ describe('DiffsStoreActions', () => { [ { type: types.ADD_CONTEXT_LINES, - payload: { lineNumbers, contextLines, params, fileHash }, + payload: { lineNumbers, contextLines, params, fileHash, isExpandDown, nextLineNumbers }, }, ], [], @@ -1080,4 +1091,30 @@ describe('DiffsStoreActions', () => { ); }); }); + + describe('setSuggestPopoverDismissed', () => { + it('commits SET_SHOW_SUGGEST_POPOVER', done => { + const state = { dismissEndpoint: `${gl.TEST_HOST}/-/user_callouts` }; + const mock = new MockAdapter(axios); + mock.onPost(state.dismissEndpoint).reply(200, {}); + + spyOn(axios, 'post').and.callThrough(); + + testAction( + setSuggestPopoverDismissed, + null, + state, + [{ type: types.SET_SHOW_SUGGEST_POPOVER }], + [], + () => { + expect(axios.post).toHaveBeenCalledWith(state.dismissEndpoint, { + feature_name: 'suggest_popover_dismissed', + }); + + mock.restore(); + done(); + }, + ); + }); + }); }); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index fa193e1d3b9..3e033b6c9dc 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -81,6 +81,8 @@ describe('DiffsStoreMutations', () => { params: { bottom: true, }, + isExpandDown: false, + nextLineNumbers: {}, }; const diffFile = { file_hash: options.fileHash, @@ -108,6 +110,8 @@ describe('DiffsStoreMutations', () => { options.contextLines, options.lineNumbers, options.params.bottom, + options.isExpandDown, + options.nextLineNumbers, ); expect(addContextLinesSpy).toHaveBeenCalledWith({ @@ -116,6 +120,7 @@ describe('DiffsStoreMutations', () => { contextLines: options.contextLines, bottom: options.params.bottom, lineNumbers: options.lineNumbers, + isExpandDown: false, }); }); }); @@ -850,4 +855,14 @@ describe('DiffsStoreMutations', () => { expect(file.renderingLines).toBe(false); }); }); + + describe('SET_SHOW_SUGGEST_POPOVER', () => { + it('sets showSuggestPopover to false', () => { + const state = { showSuggestPopover: true }; + + mutations[types.SET_SHOW_SUGGEST_POPOVER](state); + + expect(state.showSuggestPopover).toBe(false); + }); + }); }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index 1f877910125..65eb4c9d2a3 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -260,6 +260,17 @@ describe('DiffsStoreUtils', () => { expect(linesWithReferences[1].meta_data.old_pos).toEqual(2); expect(linesWithReferences[1].meta_data.new_pos).toEqual(3); }); + + it('should add correct line references when isExpandDown is true', () => { + const lines = [{ type: null }, { type: MATCH_LINE_TYPE }]; + const linesWithReferences = utils.addLineReferences(lines, lineNumbers, false, true, { + old_line: 10, + new_line: 11, + }); + + expect(linesWithReferences[1].meta_data.old_pos).toEqual(10); + expect(linesWithReferences[1].meta_data.new_pos).toEqual(11); + }); }); describe('trimFirstCharOfLineContent', () => { diff --git a/spec/javascripts/environments/confirm_rollback_modal_spec.js b/spec/javascripts/environments/confirm_rollback_modal_spec.js deleted file mode 100644 index 05715bce38f..00000000000 --- a/spec/javascripts/environments/confirm_rollback_modal_spec.js +++ /dev/null @@ -1,70 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlModal } from '@gitlab/ui'; -import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue'; -import eventHub from '~/environments/event_hub'; - -describe('Confirm Rollback Modal Component', () => { - let environment; - - beforeEach(() => { - environment = { - name: 'test', - last_deployment: { - commit: { - short_id: 'abc0123', - }, - }, - modalId: 'test', - }; - }); - - it('should show "Rollback" when isLastDeployment is false', () => { - const component = shallowMount(ConfirmRollbackModal, { - propsData: { - environment: { - ...environment, - isLastDeployment: false, - }, - }, - }); - const modal = component.find(GlModal); - - expect(modal.attributes('title')).toContain('Rollback'); - expect(modal.attributes('title')).toContain('test'); - expect(modal.attributes('ok-title')).toBe('Rollback'); - expect(modal.text()).toContain('commit abc0123'); - expect(modal.text()).toContain('Are you sure you want to continue?'); - }); - - it('should show "Re-deploy" when isLastDeployment is true', () => { - const component = shallowMount(ConfirmRollbackModal, { - propsData: { - environment: { - ...environment, - isLastDeployment: true, - }, - }, - }); - const modal = component.find(GlModal); - - expect(modal.attributes('title')).toContain('Re-deploy'); - expect(modal.attributes('title')).toContain('test'); - expect(modal.attributes('ok-title')).toBe('Re-deploy'); - expect(modal.text()).toContain('commit abc0123'); - expect(modal.text()).toContain('Are you sure you want to continue?'); - }); - - it('should emit the "rollback" event when "ok" is clicked', () => { - environment = { ...environment, isLastDeployment: true }; - const component = shallowMount(ConfirmRollbackModal, { - propsData: { - environment, - }, - }); - const eventHubSpy = spyOn(eventHub, '$emit'); - const modal = component.find(GlModal); - modal.vm.$emit('ok'); - - expect(eventHubSpy).toHaveBeenCalledWith('rollbackEnvironment', environment); - }); -}); diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js deleted file mode 100644 index 8c47f6a12c0..00000000000 --- a/spec/javascripts/environments/environment_rollback_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import Vue from 'vue'; -import { shallowMount } from '@vue/test-utils'; -import { GlButton } from '@gitlab/ui'; -import eventHub from '~/environments/event_hub'; -import rollbackComp from '~/environments/components/environment_rollback.vue'; - -describe('Rollback Component', () => { - const retryUrl = 'https://gitlab.com/retry'; - let RollbackComponent; - - beforeEach(() => { - RollbackComponent = Vue.extend(rollbackComp); - }); - - it('Should render Re-deploy label when isLastDeployment is true', () => { - const component = new RollbackComponent({ - el: document.querySelector('.test-dom-element'), - propsData: { - retryUrl, - isLastDeployment: true, - environment: {}, - }, - }).$mount(); - - expect(component.$el).toHaveSpriteIcon('repeat'); - }); - - it('Should render Rollback label when isLastDeployment is false', () => { - const component = new RollbackComponent({ - el: document.querySelector('.test-dom-element'), - propsData: { - retryUrl, - isLastDeployment: false, - environment: {}, - }, - }).$mount(); - - expect(component.$el).toHaveSpriteIcon('redo'); - }); - - it('should emit a "rollback" event on button click', () => { - const eventHubSpy = spyOn(eventHub, '$emit'); - const component = shallowMount(RollbackComponent, { - propsData: { - retryUrl, - environment: { - name: 'test', - }, - }, - }); - const button = component.find(GlButton); - - button.vm.$emit('click'); - - expect(eventHubSpy).toHaveBeenCalledWith('requestRollbackEnvironment', { - retryUrl, - isLastDeployment: true, - name: 'test', - }); - }); -}); diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js index 56e18db59c5..fc98e656efe 100644 --- a/spec/javascripts/environments/environment_terminal_button_spec.js +++ b/spec/javascripts/environments/environment_terminal_button_spec.js @@ -12,36 +12,24 @@ describe('Stop Component', () => { }).$mount(); }; - describe('enabled', () => { - beforeEach(() => { - mountWithProps({ terminalPath }); - }); - - describe('computed', () => { - it('title', () => { - expect(component.title).toEqual('Terminal'); - }); - }); - - it('should render a link to open a web terminal with the provided path', () => { - expect(component.$el.tagName).toEqual('A'); - expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal'); - expect(component.$el.getAttribute('aria-label')).toEqual('Terminal'); - expect(component.$el.getAttribute('href')).toEqual(terminalPath); - }); + beforeEach(() => { + mountWithProps({ terminalPath }); + }); - it('should render a non-disabled button', () => { - expect(component.$el.classList).not.toContain('disabled'); + describe('computed', () => { + it('title', () => { + expect(component.title).toEqual('Terminal'); }); }); - describe('disabled', () => { - beforeEach(() => { - mountWithProps({ terminalPath, disabled: true }); - }); + it('should render a link to open a web terminal with the provided path', () => { + expect(component.$el.tagName).toEqual('A'); + expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal'); + expect(component.$el.getAttribute('aria-label')).toEqual('Terminal'); + expect(component.$el.getAttribute('href')).toEqual(terminalPath); + }); - it('should render a disabled button', () => { - expect(component.$el.classList).toContain('disabled'); - }); + it('should render a non-disabled button', () => { + expect(component.$el.classList).not.toContain('disabled'); }); }); diff --git a/spec/javascripts/error_tracking_settings/components/app_spec.js b/spec/javascripts/error_tracking_settings/components/app_spec.js deleted file mode 100644 index 2e52a45fd34..00000000000 --- a/spec/javascripts/error_tracking_settings/components/app_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import Vuex from 'vuex'; -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import ErrorTrackingSettings from '~/error_tracking_settings/components/app.vue'; -import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue'; -import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue'; -import createStore from '~/error_tracking_settings/store'; -import { TEST_HOST } from 'spec/test_constants'; - -const localVue = createLocalVue(); -localVue.use(Vuex); - -describe('error tracking settings app', () => { - let store; - let wrapper; - - function mountComponent() { - wrapper = shallowMount(ErrorTrackingSettings, { - localVue, - store, // Override the imported store - propsData: { - initialEnabled: 'true', - initialApiHost: TEST_HOST, - initialToken: 'someToken', - initialProject: null, - listProjectsEndpoint: TEST_HOST, - operationsSettingsEndpoint: TEST_HOST, - }, - }); - } - - beforeEach(() => { - store = createStore(); - - mountComponent(); - }); - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - describe('section', () => { - it('renders the form and dropdown', () => { - expect(wrapper.find(ErrorTrackingForm).exists()).toBeTruthy(); - expect(wrapper.find(ProjectDropdown).exists()).toBeTruthy(); - }); - - it('renders the Save Changes button', () => { - expect(wrapper.find('.js-error-tracking-button').exists()).toBeTruthy(); - }); - - it('enables the button by default', () => { - expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeFalsy(); - }); - - it('disables the button when saving', () => { - store.state.settingsLoading = true; - - expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeTruthy(); - }); - }); -}); diff --git a/spec/javascripts/error_tracking_settings/components/error_tracking_form_spec.js b/spec/javascripts/error_tracking_settings/components/error_tracking_form_spec.js deleted file mode 100644 index 23e57c4bbf1..00000000000 --- a/spec/javascripts/error_tracking_settings/components/error_tracking_form_spec.js +++ /dev/null @@ -1,91 +0,0 @@ -import Vuex from 'vuex'; -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import { GlButton, GlFormInput } from '@gitlab/ui'; -import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue'; -import { defaultProps } from '../mock'; - -const localVue = createLocalVue(); -localVue.use(Vuex); - -describe('error tracking settings form', () => { - let wrapper; - - function mountComponent() { - wrapper = shallowMount(ErrorTrackingForm, { - localVue, - propsData: defaultProps, - }); - } - - beforeEach(() => { - mountComponent(); - }); - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - describe('an empty form', () => { - it('is rendered', () => { - expect(wrapper.findAll(GlFormInput).length).toBe(2); - expect(wrapper.find(GlFormInput).attributes('id')).toBe('error-tracking-api-host'); - expect( - wrapper - .findAll(GlFormInput) - .at(1) - .attributes('id'), - ).toBe('error-tracking-token'); - - expect(wrapper.findAll(GlButton).exists()).toBe(true); - }); - - it('is rendered with labels and placeholders', () => { - const pageText = wrapper.text(); - - expect(pageText).toContain('Find your hostname in your Sentry account settings page'); - expect(pageText).toContain( - "After adding your Auth Token, use the 'Connect' button to load projects", - ); - - expect(pageText).not.toContain('Connection has failed. Re-check Auth Token and try again'); - expect( - wrapper - .findAll(GlFormInput) - .at(0) - .attributes('placeholder'), - ).toContain('https://mysentryserver.com'); - }); - }); - - describe('after a successful connection', () => { - beforeEach(() => { - wrapper.setProps({ connectSuccessful: true }); - }); - - it('shows the success checkmark', () => { - expect(wrapper.find('.js-error-tracking-connect-success').isVisible()).toBe(true); - }); - - it('does not show an error', () => { - expect(wrapper.text()).not.toContain( - 'Connection has failed. Re-check Auth Token and try again', - ); - }); - }); - - describe('after an unsuccessful connection', () => { - beforeEach(() => { - wrapper.setProps({ connectError: true }); - }); - - it('does not show the check mark', () => { - expect(wrapper.find('.js-error-tracking-connect-success').isVisible()).toBe(false); - }); - - it('shows an error', () => { - expect(wrapper.text()).toContain('Connection has failed. Re-check Auth Token and try again'); - }); - }); -}); diff --git a/spec/javascripts/error_tracking_settings/components/project_dropdown_spec.js b/spec/javascripts/error_tracking_settings/components/project_dropdown_spec.js deleted file mode 100644 index 8e5dbe28452..00000000000 --- a/spec/javascripts/error_tracking_settings/components/project_dropdown_spec.js +++ /dev/null @@ -1,109 +0,0 @@ -import _ from 'underscore'; -import Vuex from 'vuex'; -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue'; -import { defaultProps, projectList, staleProject } from '../mock'; - -const localVue = createLocalVue(); -localVue.use(Vuex); - -describe('error tracking settings project dropdown', () => { - let wrapper; - - function mountComponent() { - wrapper = shallowMount(ProjectDropdown, { - localVue, - propsData: { - ..._.pick( - defaultProps, - 'dropdownLabel', - 'invalidProjectLabel', - 'projects', - 'projectSelectionLabel', - 'selectedProject', - 'token', - ), - hasProjects: false, - isProjectInvalid: false, - }, - }); - } - - beforeEach(() => { - mountComponent(); - }); - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - describe('empty project list', () => { - it('renders the dropdown', () => { - expect(wrapper.find('#project-dropdown').exists()).toBeTruthy(); - expect(wrapper.find(GlDropdown).exists()).toBeTruthy(); - }); - - it('shows helper text', () => { - expect(wrapper.find('.js-project-dropdown-label').exists()).toBeTruthy(); - expect(wrapper.find('.js-project-dropdown-label').text()).toContain( - 'To enable project selection', - ); - }); - - it('does not show an error', () => { - expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy(); - }); - - it('does not contain any dropdown items', () => { - expect(wrapper.find(GlDropdownItem).exists()).toBeFalsy(); - expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available'); - }); - }); - - describe('populated project list', () => { - beforeEach(() => { - wrapper.setProps({ projects: _.clone(projectList), hasProjects: true }); - }); - - it('renders the dropdown', () => { - expect(wrapper.find('#project-dropdown').exists()).toBeTruthy(); - expect(wrapper.find(GlDropdown).exists()).toBeTruthy(); - }); - - it('contains a number of dropdown items', () => { - expect(wrapper.find(GlDropdownItem).exists()).toBeTruthy(); - expect(wrapper.findAll(GlDropdownItem).length).toBe(2); - }); - }); - - describe('selected project', () => { - const selectedProject = _.clone(projectList[0]); - - beforeEach(() => { - wrapper.setProps({ projects: _.clone(projectList), selectedProject, hasProjects: true }); - }); - - it('does not show helper text', () => { - expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy(); - expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy(); - }); - }); - - describe('invalid project selected', () => { - beforeEach(() => { - wrapper.setProps({ - projects: _.clone(projectList), - selectedProject: staleProject, - isProjectInvalid: true, - }); - }); - - it('displays a error', () => { - expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy(); - expect(wrapper.find('.js-project-dropdown-error').exists()).toBeTruthy(); - }); - }); -}); diff --git a/spec/javascripts/error_tracking_settings/mock.js b/spec/javascripts/error_tracking_settings/mock.js deleted file mode 100644 index 32cdba33c14..00000000000 --- a/spec/javascripts/error_tracking_settings/mock.js +++ /dev/null @@ -1,92 +0,0 @@ -import createStore from '~/error_tracking_settings/store'; -import { TEST_HOST } from 'spec/test_constants'; - -const defaultStore = createStore(); - -export const projectList = [ - { - name: 'name', - slug: 'slug', - organizationName: 'organizationName', - organizationSlug: 'organizationSlug', - }, - { - name: 'name2', - slug: 'slug2', - organizationName: 'organizationName2', - organizationSlug: 'organizationSlug2', - }, -]; - -export const staleProject = { - name: 'staleName', - slug: 'staleSlug', - organizationName: 'staleOrganizationName', - organizationSlug: 'staleOrganizationSlug', -}; - -export const normalizedProject = { - name: 'name', - slug: 'slug', - organizationName: 'organization_name', - organizationSlug: 'organization_slug', -}; - -export const sampleBackendProject = { - name: normalizedProject.name, - slug: normalizedProject.slug, - organization_name: normalizedProject.organizationName, - organization_slug: normalizedProject.organizationSlug, -}; - -export const sampleFrontendSettings = { - apiHost: 'apiHost', - enabled: false, - token: 'token', - selectedProject: { - slug: normalizedProject.slug, - name: normalizedProject.name, - organizationName: normalizedProject.organizationName, - organizationSlug: normalizedProject.organizationSlug, - }, -}; - -export const transformedSettings = { - api_host: 'apiHost', - enabled: false, - token: 'token', - project: { - slug: normalizedProject.slug, - name: normalizedProject.name, - organization_name: normalizedProject.organizationName, - organization_slug: normalizedProject.organizationSlug, - }, -}; - -export const defaultProps = { - ...defaultStore.state, - ...defaultStore.getters, -}; - -export const initialEmptyState = { - apiHost: '', - enabled: false, - project: null, - token: '', - listProjectsEndpoint: TEST_HOST, - operationsSettingsEndpoint: TEST_HOST, -}; - -export const initialPopulatedState = { - apiHost: 'apiHost', - enabled: true, - project: JSON.stringify(projectList[0]), - token: 'token', - listProjectsEndpoint: TEST_HOST, - operationsSettingsEndpoint: TEST_HOST, -}; - -export const projectWithHtmlTemplate = { - ...projectList[0], - name: '<strong>bold</strong>', -}; diff --git a/spec/javascripts/error_tracking_settings/store/actions_spec.js b/spec/javascripts/error_tracking_settings/store/actions_spec.js deleted file mode 100644 index 0255b3a7aa4..00000000000 --- a/spec/javascripts/error_tracking_settings/store/actions_spec.js +++ /dev/null @@ -1,191 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'spec/helpers/vuex_action_helper'; -import { TEST_HOST } from 'spec/test_constants'; -import axios from '~/lib/utils/axios_utils'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import actionsDefaultExport, * as actions from '~/error_tracking_settings/store/actions'; -import * as types from '~/error_tracking_settings/store/mutation_types'; -import defaultState from '~/error_tracking_settings/store/state'; -import { projectList } from '../mock'; - -describe('error tracking settings actions', () => { - let state; - - describe('project list actions', () => { - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - state = { ...defaultState(), listProjectsEndpoint: TEST_HOST }; - }); - - afterEach(() => { - mock.restore(); - }); - - it('should request and transform the project list', done => { - mock.onPost(TEST_HOST).reply(() => [200, { projects: projectList }]); - testAction( - actions.fetchProjects, - null, - state, - [], - [ - { type: 'requestProjects' }, - { - type: 'receiveProjectsSuccess', - payload: projectList.map(convertObjectPropsToCamelCase), - }, - ], - () => { - expect(mock.history.post.length).toBe(1); - done(); - }, - ); - }); - - it('should handle a server error', done => { - mock.onPost(`${TEST_HOST}.json`).reply(() => [400]); - testAction( - actions.fetchProjects, - null, - state, - [], - [ - { type: 'requestProjects' }, - { - type: 'receiveProjectsError', - }, - ], - () => { - expect(mock.history.post.length).toBe(1); - done(); - }, - ); - }); - - it('should request projects correctly', done => { - testAction(actions.requestProjects, null, state, [{ type: types.RESET_CONNECT }], [], done); - }); - - it('should receive projects correctly', done => { - const testPayload = []; - testAction( - actions.receiveProjectsSuccess, - testPayload, - state, - [ - { type: types.UPDATE_CONNECT_SUCCESS }, - { type: types.RECEIVE_PROJECTS, payload: testPayload }, - ], - [], - done, - ); - }); - - it('should handle errors when receiving projects', done => { - const testPayload = []; - testAction( - actions.receiveProjectsError, - testPayload, - state, - [{ type: types.UPDATE_CONNECT_ERROR }, { type: types.CLEAR_PROJECTS }], - [], - done, - ); - }); - }); - - describe('save changes actions', () => { - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - state = { - operationsSettingsEndpoint: TEST_HOST, - }; - }); - - afterEach(() => { - mock.restore(); - }); - - it('should save the page', done => { - const refreshCurrentPage = spyOnDependency(actionsDefaultExport, 'refreshCurrentPage'); - mock.onPatch(TEST_HOST).reply(200); - testAction(actions.updateSettings, null, state, [], [{ type: 'requestSettings' }], () => { - expect(mock.history.patch.length).toBe(1); - expect(refreshCurrentPage).toHaveBeenCalled(); - done(); - }); - }); - - it('should handle a server error', done => { - mock.onPatch(TEST_HOST).reply(400); - testAction( - actions.updateSettings, - null, - state, - [], - [ - { type: 'requestSettings' }, - { - type: 'receiveSettingsError', - payload: new Error('Request failed with status code 400'), - }, - ], - () => { - expect(mock.history.patch.length).toBe(1); - done(); - }, - ); - }); - - it('should request to save the page', done => { - testAction( - actions.requestSettings, - null, - state, - [{ type: types.UPDATE_SETTINGS_LOADING, payload: true }], - [], - done, - ); - }); - - it('should handle errors when requesting to save the page', done => { - testAction( - actions.receiveSettingsError, - {}, - state, - [{ type: types.UPDATE_SETTINGS_LOADING, payload: false }], - [], - done, - ); - }); - }); - - describe('generic actions to update the store', () => { - const testData = 'test'; - it('should reset the `connect success` flag when updating the api host', done => { - testAction( - actions.updateApiHost, - testData, - state, - [{ type: types.UPDATE_API_HOST, payload: testData }, { type: types.RESET_CONNECT }], - [], - done, - ); - }); - - it('should reset the `connect success` flag when updating the token', done => { - testAction( - actions.updateToken, - testData, - state, - [{ type: types.UPDATE_TOKEN, payload: testData }, { type: types.RESET_CONNECT }], - [], - done, - ); - }); - }); -}); diff --git a/spec/javascripts/error_tracking_settings/store/getters_spec.js b/spec/javascripts/error_tracking_settings/store/getters_spec.js deleted file mode 100644 index 2c5ff084b8a..00000000000 --- a/spec/javascripts/error_tracking_settings/store/getters_spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import * as getters from '~/error_tracking_settings/store/getters'; -import defaultState from '~/error_tracking_settings/store/state'; -import { projectList, projectWithHtmlTemplate, staleProject } from '../mock'; - -describe('Error Tracking Settings - Getters', () => { - let state; - - beforeEach(() => { - state = defaultState(); - }); - - describe('hasProjects', () => { - it('should reflect when no projects exist', () => { - expect(getters.hasProjects(state)).toEqual(false); - }); - - it('should reflect when projects exist', () => { - state.projects = projectList; - - expect(getters.hasProjects(state)).toEqual(true); - }); - }); - - describe('isProjectInvalid', () => { - const mockGetters = { hasProjects: true }; - it('should show when a project is valid', () => { - state.projects = projectList; - [state.selectedProject] = projectList; - - expect(getters.isProjectInvalid(state, mockGetters)).toEqual(false); - }); - - it('should show when a project is invalid', () => { - state.projects = projectList; - state.selectedProject = staleProject; - - expect(getters.isProjectInvalid(state, mockGetters)).toEqual(true); - }); - }); - - describe('dropdownLabel', () => { - const mockGetters = { hasProjects: false }; - it('should display correctly when there are no projects available', () => { - expect(getters.dropdownLabel(state, mockGetters)).toEqual('No projects available'); - }); - - it('should display correctly when a project is selected', () => { - [state.selectedProject] = projectList; - - expect(getters.dropdownLabel(state, mockGetters)).toEqual('organizationName | name'); - }); - - it('should display correctly when no project is selected', () => { - state.projects = projectList; - - expect(getters.dropdownLabel(state, { hasProjects: true })).toEqual('Select project'); - }); - }); - - describe('invalidProjectLabel', () => { - it('should display an error containing the project name', () => { - [state.selectedProject] = projectList; - - expect(getters.invalidProjectLabel(state)).toEqual( - 'Project "name" is no longer available. Select another project to continue.', - ); - }); - - it('should properly escape the label text', () => { - state.selectedProject = projectWithHtmlTemplate; - - expect(getters.invalidProjectLabel(state)).toEqual( - 'Project "<strong>bold</strong>" is no longer available. Select another project to continue.', - ); - }); - }); - - describe('projectSelectionLabel', () => { - it('should show the correct message when the token is empty', () => { - expect(getters.projectSelectionLabel(state)).toEqual( - 'To enable project selection, enter a valid Auth Token', - ); - }); - - it('should show the correct message when token exists', () => { - state.token = 'test-token'; - - expect(getters.projectSelectionLabel(state)).toEqual( - "Click 'Connect' to re-establish the connection to Sentry and activate the dropdown.", - ); - }); - }); -}); diff --git a/spec/javascripts/error_tracking_settings/store/mutation_spec.js b/spec/javascripts/error_tracking_settings/store/mutation_spec.js deleted file mode 100644 index bb1f1da784e..00000000000 --- a/spec/javascripts/error_tracking_settings/store/mutation_spec.js +++ /dev/null @@ -1,82 +0,0 @@ -import { TEST_HOST } from 'spec/test_constants'; -import mutations from '~/error_tracking_settings/store/mutations'; -import defaultState from '~/error_tracking_settings/store/state'; -import * as types from '~/error_tracking_settings/store/mutation_types'; -import { - initialEmptyState, - initialPopulatedState, - projectList, - sampleBackendProject, - normalizedProject, -} from '../mock'; - -describe('error tracking settings mutations', () => { - describe('mutations', () => { - let state; - - beforeEach(() => { - state = defaultState(); - }); - - it('should create an empty initial state correctly', () => { - mutations[types.SET_INITIAL_STATE](state, { - ...initialEmptyState, - }); - - expect(state.apiHost).toEqual(''); - expect(state.enabled).toEqual(false); - expect(state.selectedProject).toEqual(null); - expect(state.token).toEqual(''); - expect(state.listProjectsEndpoint).toEqual(TEST_HOST); - expect(state.operationsSettingsEndpoint).toEqual(TEST_HOST); - }); - - it('should populate the initial state correctly', () => { - mutations[types.SET_INITIAL_STATE](state, { - ...initialPopulatedState, - }); - - expect(state.apiHost).toEqual('apiHost'); - expect(state.enabled).toEqual(true); - expect(state.selectedProject).toEqual(projectList[0]); - expect(state.token).toEqual('token'); - expect(state.listProjectsEndpoint).toEqual(TEST_HOST); - expect(state.operationsSettingsEndpoint).toEqual(TEST_HOST); - }); - - it('should receive projects successfully', () => { - mutations[types.RECEIVE_PROJECTS](state, [sampleBackendProject]); - - expect(state.projects).toEqual([normalizedProject]); - }); - - it('should strip out unnecessary project properties', () => { - mutations[types.RECEIVE_PROJECTS](state, [ - { ...sampleBackendProject, extra_property: 'extra_property' }, - ]); - - expect(state.projects).toEqual([normalizedProject]); - }); - - it('should update state when connect is successful', () => { - mutations[types.UPDATE_CONNECT_SUCCESS](state); - - expect(state.connectSuccessful).toBe(true); - expect(state.connectError).toBe(false); - }); - - it('should update state when connect fails', () => { - mutations[types.UPDATE_CONNECT_ERROR](state); - - expect(state.connectSuccessful).toBe(false); - expect(state.connectError).toBe(true); - }); - - it('should update state when connect is reset', () => { - mutations[types.RESET_CONNECT](state); - - expect(state.connectSuccessful).toBe(false); - expect(state.connectError).toBe(false); - }); - }); -}); diff --git a/spec/javascripts/error_tracking_settings/utils_spec.js b/spec/javascripts/error_tracking_settings/utils_spec.js deleted file mode 100644 index 4b144f7daf1..00000000000 --- a/spec/javascripts/error_tracking_settings/utils_spec.js +++ /dev/null @@ -1,29 +0,0 @@ -import { transformFrontendSettings } from '~/error_tracking_settings/utils'; -import { sampleFrontendSettings, transformedSettings } from './mock'; - -describe('error tracking settings utils', () => { - describe('data transform functions', () => { - it('should transform settings successfully for the backend', () => { - expect(transformFrontendSettings(sampleFrontendSettings)).toEqual(transformedSettings); - }); - - it('should transform empty values in the settings object to null', () => { - const emptyFrontendSettingsObject = { - apiHost: '', - enabled: false, - token: '', - selectedProject: null, - }; - const transformedEmptySettingsObject = { - api_host: null, - enabled: false, - token: null, - project: null, - }; - - expect(transformFrontendSettings(emptyFrontendSettingsObject)).toEqual( - transformedEmptySettingsObject, - ); - }); - }); -}); diff --git a/spec/javascripts/filtered_search/visual_token_value_spec.js b/spec/javascripts/filtered_search/visual_token_value_spec.js index d1d16afc977..10d844fd94b 100644 --- a/spec/javascripts/filtered_search/visual_token_value_spec.js +++ b/spec/javascripts/filtered_search/visual_token_value_spec.js @@ -155,7 +155,7 @@ describe('Filtered Search Visual Tokens', () => { `); const filteredSearchInput = document.querySelector('.filtered-search'); - filteredSearchInput.dataset.baseEndpoint = dummyEndpoint; + filteredSearchInput.dataset.runnerTagsEndpoint = `${dummyEndpoint}/admin/runners/tag_list`; filteredSearchInput.dataset.labelsEndpoint = `${dummyEndpoint}/-/labels`; filteredSearchInput.dataset.milestonesEndpoint = `${dummyEndpoint}/-/milestones`; diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore index bed020f5b0a..d6b7ef32c84 100644 --- a/spec/javascripts/fixtures/.gitignore +++ b/spec/javascripts/fixtures/.gitignore @@ -1,5 +1,2 @@ -*.html.raw -*.html -*.json -*.pdf -*.bmpr +* +!.gitignore diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb deleted file mode 100644 index e0aaecf626a..00000000000 --- a/spec/javascripts/fixtures/abuse_reports.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'spec_helper' - -describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let!(:abuse_report) { create(:abuse_report) } - let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') } - let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) } - - render_views - - before(:all) do - clean_frontend_fixtures('abuse_reports/') - end - - before do - sign_in(admin) - end - - it 'abuse_reports/abuse_reports_list.html' do - get :index - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/admin_users.rb b/spec/javascripts/fixtures/admin_users.rb deleted file mode 100644 index 22a5de66577..00000000000 --- a/spec/javascripts/fixtures/admin_users.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe Admin::UsersController, '(JavaScript fixtures)', type: :controller do - include StubENV - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - - before do - stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - sign_in(admin) - end - - render_views - - before(:all) do - clean_frontend_fixtures('admin/users') - end - - it 'admin/users/new_with_internal_user_regex.html' do - stub_application_setting(user_default_external: true) - stub_application_setting(user_default_internal_regex: '^(?:(?!\.ext@).)*$\r?') - - get :new - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/application_settings.rb b/spec/javascripts/fixtures/application_settings.rb deleted file mode 100644 index d4651fa6ece..00000000000 --- a/spec/javascripts/fixtures/application_settings.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' - -describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', type: :controller do - include StubENV - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project_empty_repo, namespace: namespace, path: 'application-settings') } - - before do - stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - sign_in(admin) - end - - render_views - - before(:all) do - clean_frontend_fixtures('application_settings/') - end - - after do - remove_repository(project) - end - - it 'application_settings/accounts_and_limit.html' do - stub_application_setting(user_default_external: false) - - get :show - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/autocomplete_sources.rb b/spec/javascripts/fixtures/autocomplete_sources.rb deleted file mode 100644 index b20a0159d7d..00000000000 --- a/spec/javascripts/fixtures/autocomplete_sources.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - set(:admin) { create(:admin) } - set(:group) { create(:group, name: 'frontend-fixtures') } - set(:project) { create(:project, namespace: group, path: 'autocomplete-sources-project') } - set(:issue) { create(:issue, project: project) } - - before(:all) do - clean_frontend_fixtures('autocomplete_sources/') - end - - before do - sign_in(admin) - end - - it 'autocomplete_sources/labels.json' do - issue.labels << create(:label, project: project, title: 'bug') - issue.labels << create(:label, project: project, title: 'critical') - - create(:label, project: project, title: 'feature') - create(:label, project: project, title: 'documentation') - - get :labels, - format: :json, - params: { - namespace_id: group.path, - project_id: project.path, - type: issue.class.name, - type_id: issue.id - } - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb deleted file mode 100644 index 07670552cd5..00000000000 --- a/spec/javascripts/fixtures/blob.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' - -describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } - - render_views - - before(:all) do - clean_frontend_fixtures('blob/') - end - - before do - sign_in(admin) - allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') - end - - after do - remove_repository(project) - end - - it 'blob/show.html' do - get(:show, params: { - namespace_id: project.namespace, - project_id: project, - id: 'add-ipython-files/files/ipython/basic.ipynb' - }) - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/boards.rb b/spec/javascripts/fixtures/boards.rb deleted file mode 100644 index 5835721d3d5..00000000000 --- a/spec/javascripts/fixtures/boards.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'boards-project') } - - render_views - - before(:all) do - clean_frontend_fixtures('boards/') - end - - before do - sign_in(admin) - end - - it 'boards/show.html' do - get(:index, params: { - namespace_id: project.namespace, - project_id: project - }) - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb deleted file mode 100644 index 204aa9b7c7a..00000000000 --- a/spec/javascripts/fixtures/branches.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } - - render_views - - before(:all) do - clean_frontend_fixtures('branches/') - end - - before do - sign_in(admin) - end - - after do - remove_repository(project) - end - - it 'branches/new_branch.html' do - get :new, params: { - namespace_id: project.namespace.to_param, - project_id: project - } - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb deleted file mode 100644 index 1076404e0e3..00000000000 --- a/spec/javascripts/fixtures/clusters.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' - -describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace) } - let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } - - render_views - - before(:all) do - clean_frontend_fixtures('clusters/') - end - - before do - sign_in(admin) - end - - after do - remove_repository(project) - end - - it 'clusters/show_cluster.html' do - get :show, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: cluster - } - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb deleted file mode 100644 index ff9a4bc1adc..00000000000 --- a/spec/javascripts/fixtures/commit.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' - -describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - set(:project) { create(:project, :repository) } - set(:user) { create(:user) } - let(:commit) { project.commit("master") } - - render_views - - before(:all) do - clean_frontend_fixtures('commit/') - end - - before do - project.add_maintainer(user) - sign_in(user) - allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') - end - - it 'commit/show.html' do - params = { - namespace_id: project.namespace, - project_id: project, - id: commit.id - } - - get :show, params: params - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb deleted file mode 100644 index 38eab853da2..00000000000 --- a/spec/javascripts/fixtures/deploy_keys.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'spec_helper' - -describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } - let(:project2) { create(:project, :internal)} - let(:project3) { create(:project, :internal)} - let(:project4) { create(:project, :internal)} - - before(:all) do - clean_frontend_fixtures('deploy_keys/') - end - - before do - sign_in(admin) - end - - after do - remove_repository(project) - end - - render_views - - it 'deploy_keys/keys.json' do - create(:rsa_deploy_key_2048, public: true) - project_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com') - internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com') - create(:deploy_keys_project, project: project, deploy_key: project_key) - create(:deploy_keys_project, project: project2, deploy_key: internal_key) - create(:deploy_keys_project, project: project3, deploy_key: project_key) - create(:deploy_keys_project, project: project4, deploy_key: project_key) - - get :index, params: { - namespace_id: project.namespace.to_param, - project_id: project - }, format: :json - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb deleted file mode 100644 index 4d0afc3ce1a..00000000000 --- a/spec/javascripts/fixtures/groups.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -describe 'Groups (JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre')} - - render_views - - before(:all) do - clean_frontend_fixtures('groups/') - end - - before do - group.add_maintainer(admin) - sign_in(admin) - end - - describe GroupsController, '(JavaScript fixtures)', type: :controller do - it 'groups/edit.html' do - get :edit, params: { id: group } - - expect(response).to be_success - end - end - - describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do - it 'groups/ci_cd_settings.html' do - get :show, params: { group_id: group } - - expect(response).to be_success - end - end -end diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb deleted file mode 100644 index d8d77f767de..00000000000 --- a/spec/javascripts/fixtures/issues.rb +++ /dev/null @@ -1,122 +0,0 @@ -require 'spec_helper' - -describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin, feed_token: 'feedtoken:coldfeed') } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') } - - render_views - - before(:all) do - clean_frontend_fixtures('issues/') - end - - before do - sign_in(admin) - end - - after do - remove_repository(project) - end - - it 'issues/open-issue.html' do - render_issue(create(:issue, project: project)) - end - - it 'issues/closed-issue.html' do - render_issue(create(:closed_issue, project: project)) - end - - it 'issues/issue-with-task-list.html' do - issue = create(:issue, project: project, description: '- [ ] Task List Item') - render_issue(issue) - end - - it 'issues/issue_with_comment.html' do - issue = create(:issue, project: project) - create(:note, project: project, noteable: issue, note: '- [ ] Task List Item').save - render_issue(issue) - end - - it 'issues/issue_list.html' do - create(:issue, project: project) - - get :index, params: { - namespace_id: project.namespace.to_param, - project_id: project - } - - expect(response).to be_success - end - - private - - def render_issue(issue) - get :show, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: issue.to_param - } - - expect(response).to be_success - end -end - -describe API::Issues, '(JavaScript fixtures)', type: :request do - include ApiHelpers - include JavaScriptFixturesHelpers - - def get_related_merge_requests(project_id, issue_iid, user = nil) - get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user) - end - - def create_referencing_mr(user, project, issue) - attributes = { - author: user, - source_project: project, - target_project: project, - source_branch: "master", - target_branch: "test", - assignee: user, - description: "See #{issue.to_reference}" - } - create(:merge_request, attributes).tap do |merge_request| - create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) - end - end - - it 'issues/related_merge_requests.json' do - user = create(:user) - project = create(:project, :public, creator_id: user.id, namespace: user.namespace) - issue_title = 'foo' - issue_description = 'closed' - milestone = create(:milestone, title: '1.0.0', project: project) - issue = create :issue, - author: user, - assignees: [user], - project: project, - milestone: milestone, - created_at: generate(:past_time), - updated_at: 1.hour.ago, - title: issue_title, - description: issue_description - - project.add_reporter(user) - create_referencing_mr(user, project, issue) - - create(:merge_request, - :simple, - author: user, - source_project: project, - target_project: project, - description: "Some description") - project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace) - create_referencing_mr(user, project2, issue).update!(head_pipeline: create(:ci_pipeline)) - - get_related_merge_requests(project.id, issue.iid, user) - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb deleted file mode 100644 index 46ccd6f8c8a..00000000000 --- a/spec/javascripts/fixtures/jobs.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'spec_helper' - -describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') } - let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) } - let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) } - let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') } - let!(:pending_build) { create(:ci_build, :pending, pipeline: pipeline, stage: 'deploy') } - let!(:delayed_job) do - create(:ci_build, :scheduled, - pipeline: pipeline, - name: 'delayed job', - stage: 'test') - end - - render_views - - before(:all) do - clean_frontend_fixtures('builds/') - clean_frontend_fixtures('jobs/') - end - - before do - sign_in(admin) - end - - after do - remove_repository(project) - end - - it 'builds/build-with-artifacts.html' do - get :show, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: build_with_artifacts.to_param - } - - expect(response).to be_success - end - - it 'jobs/delayed.json' do - get :show, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: delayed_job.to_param - }, format: :json - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/labels.rb b/spec/javascripts/fixtures/labels.rb deleted file mode 100644 index 4d1b7317274..00000000000 --- a/spec/javascripts/fixtures/labels.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'spec_helper' - -describe 'Labels (JavaScript fixtures)' do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:group) { create(:group, name: 'frontend-fixtures-group' )} - let(:project) { create(:project_empty_repo, namespace: group, path: 'labels-project') } - - let!(:project_label_bug) { create(:label, project: project, title: 'bug', color: '#FF0000') } - let!(:project_label_enhancement) { create(:label, project: project, title: 'enhancement', color: '#00FF00') } - let!(:project_label_feature) { create(:label, project: project, title: 'feature', color: '#0000FF') } - - let!(:group_label_roses) { create(:group_label, group: group, title: 'roses', color: '#FF0000') } - let!(:groub_label_space) { create(:group_label, group: group, title: 'some space', color: '#FFFFFF') } - let!(:groub_label_violets) { create(:group_label, group: group, title: 'violets', color: '#0000FF') } - - before(:all) do - clean_frontend_fixtures('labels/') - end - - after do - remove_repository(project) - end - - describe Groups::LabelsController, '(JavaScript fixtures)', type: :controller do - render_views - - before do - sign_in(admin) - end - - it 'labels/group_labels.json' do - get :index, params: { - group_id: group - }, format: 'json' - - expect(response).to be_success - end - end - - describe Projects::LabelsController, '(JavaScript fixtures)', type: :controller do - render_views - - before do - sign_in(admin) - end - - it 'labels/project_labels.json' do - get :index, params: { - namespace_id: group, - project_id: project - }, format: 'json' - - expect(response).to be_success - end - end -end diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb deleted file mode 100644 index 05860be2291..00000000000 --- a/spec/javascripts/fixtures/merge_requests.rb +++ /dev/null @@ -1,134 +0,0 @@ -require 'spec_helper' - -describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') } - let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } - let(:merged_merge_request) { create(:merge_request, :merged, source_project: project, target_project: project) } - let(:pipeline) do - create( - :ci_pipeline, - project: merge_request.source_project, - ref: merge_request.source_branch, - sha: merge_request.diff_head_sha - ) - end - let(:path) { "files/ruby/popen.rb" } - let(:position) do - Gitlab::Diff::Position.new( - old_path: path, - new_path: path, - old_line: nil, - new_line: 14, - diff_refs: merge_request.diff_refs - ) - end - - render_views - - before(:all) do - clean_frontend_fixtures('merge_requests/') - end - - before do - sign_in(admin) - allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo']) - end - - after do - remove_repository(project) - end - - it 'merge_requests/merge_request_of_current_user.html' do - merge_request.update(author: admin) - - render_merge_request(merge_request) - end - - it 'merge_requests/merge_request_with_task_list.html' do - create(:ci_build, :pending, pipeline: pipeline) - - render_merge_request(merge_request) - end - - it 'merge_requests/merged_merge_request.html' do - expect_next_instance_of(MergeRequest) do |merge_request| - allow(merge_request).to receive(:source_branch_exists?).and_return(true) - allow(merge_request).to receive(:can_remove_source_branch?).and_return(true) - end - render_merge_request(merged_merge_request) - end - - it 'merge_requests/diff_comment.html' do - create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) - create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) - render_merge_request(merge_request) - end - - it 'merge_requests/merge_request_with_comment.html' do - create(:note_on_merge_request, author: admin, project: project, noteable: merge_request, note: '- [ ] Task List Item') - render_merge_request(merge_request) - end - - it 'merge_requests/discussions.json' do - create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) - render_discussions_json(merge_request) - end - - it 'merge_requests/diff_discussion.json' do - create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) - render_discussions_json(merge_request) - end - - it 'merge_requests/resolved_diff_discussion.json' do - note = create(:discussion_note_on_merge_request, :resolved, project: project, author: admin, position: position, noteable: merge_request) - create(:system_note, project: project, author: admin, noteable: merge_request, discussion_id: note.discussion.id) - - render_discussions_json(merge_request) - end - - context 'with image diff' do - let(:merge_request2) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, title: "Added images") } - let(:image_path) { "files/images/ee_repo_logo.png" } - let(:image_position) do - Gitlab::Diff::Position.new( - old_path: image_path, - new_path: image_path, - width: 100, - height: 100, - x: 1, - y: 1, - position_type: "image", - diff_refs: merge_request2.diff_refs - ) - end - - it 'merge_requests/image_diff_discussion.json' do - create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: image_position) - render_discussions_json(merge_request2) - end - end - - private - - def render_discussions_json(merge_request) - get :discussions, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.to_param - }, format: :json - end - - def render_merge_request(merge_request) - get :show, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.to_param - }, format: :html - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/merge_requests_diffs.rb b/spec/javascripts/fixtures/merge_requests_diffs.rb deleted file mode 100644 index 03b9b713fd8..00000000000 --- a/spec/javascripts/fixtures/merge_requests_diffs.rb +++ /dev/null @@ -1,70 +0,0 @@ - -require 'spec_helper' - -describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') } - let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } - let(:path) { "files/ruby/popen.rb" } - let(:selected_commit) { merge_request.all_commits[0] } - let(:position) do - Gitlab::Diff::Position.new( - old_path: path, - new_path: path, - old_line: nil, - new_line: 14, - diff_refs: merge_request.diff_refs - ) - end - - render_views - - before(:all) do - clean_frontend_fixtures('merge_request_diffs/') - end - - before do - sign_in(admin) - end - - after do - remove_repository(project) - end - - it 'merge_request_diffs/with_commit.json' do - # Create a user that matches the selected commit author - # This is so that the "author" information will be populated - create(:user, email: selected_commit.author_email, name: selected_commit.author_name) - - render_merge_request(merge_request, commit_id: selected_commit.sha) - end - - it 'merge_request_diffs/inline_changes_tab_with_comments.json' do - create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) - create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) - render_merge_request(merge_request) - end - - it 'merge_request_diffs/parallel_changes_tab_with_comments.json' do - create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) - create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) - render_merge_request(merge_request, view: 'parallel') - end - - private - - def render_merge_request(merge_request, view: 'inline', **extra_params) - get :show, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.to_param, - view: view, - **extra_params - }, format: :json - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/pipeline_schedules.rb b/spec/javascripts/fixtures/pipeline_schedules.rb deleted file mode 100644 index aecd56e6198..00000000000 --- a/spec/javascripts/fixtures/pipeline_schedules.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'spec_helper' - -describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :public, :repository) } - let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: admin) } - let!(:pipeline_schedule_populated) { create(:ci_pipeline_schedule, project: project, owner: admin) } - let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) } - let!(:pipeline_schedule_variable2) { create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule_populated) } - - render_views - - before(:all) do - clean_frontend_fixtures('pipeline_schedules/') - end - - before do - sign_in(admin) - end - - it 'pipeline_schedules/edit.html' do - get :edit, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: pipeline_schedule.id - } - - expect(response).to be_success - end - - it 'pipeline_schedules/edit_with_variables.html' do - get :edit, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: pipeline_schedule_populated.id - } - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/pipelines.rb b/spec/javascripts/fixtures/pipelines.rb deleted file mode 100644 index de6fcfe10f4..00000000000 --- a/spec/javascripts/fixtures/pipelines.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' - -describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'pipelines-project') } - let(:commit) { create(:commit, project: project) } - let(:commit_without_author) { RepoHelpers.another_sample_commit } - let!(:user) { create(:user, email: commit.author_email) } - let!(:pipeline) { create(:ci_pipeline, project: project, sha: commit.id, user: user) } - let!(:pipeline_without_author) { create(:ci_pipeline, project: project, sha: commit_without_author.id) } - let!(:pipeline_without_commit) { create(:ci_pipeline, project: project, sha: '0000') } - - render_views - - before(:all) do - clean_frontend_fixtures('pipelines/') - end - - before do - sign_in(admin) - end - - it 'pipelines/pipelines.json' do - get :index, params: { - namespace_id: namespace, - project_id: project - }, format: :json - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb deleted file mode 100644 index 94c59207898..00000000000 --- a/spec/javascripts/fixtures/projects.rb +++ /dev/null @@ -1,81 +0,0 @@ -require 'spec_helper' - -describe 'Projects (JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - runners_token = 'runnerstoken:intabulasreferre' - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token) } - let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff') } - let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) } - - render_views - - before(:all) do - clean_frontend_fixtures('projects/') - end - - before do - project.add_maintainer(admin) - sign_in(admin) - allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') - end - - after do - remove_repository(project) - end - - describe ProjectsController, '(JavaScript fixtures)', type: :controller do - it 'projects/dashboard.html' do - get :show, params: { - namespace_id: project.namespace.to_param, - id: project - } - - expect(response).to be_success - end - - it 'projects/overview.html' do - get :show, params: { - namespace_id: project_with_repo.namespace.to_param, - id: project_with_repo - } - - expect(response).to be_success - end - - it 'projects/edit.html' do - get :edit, params: { - namespace_id: project.namespace.to_param, - id: project - } - - expect(response).to be_success - end - end - - describe Projects::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do - it 'projects/ci_cd_settings.html' do - get :show, params: { - namespace_id: project.namespace.to_param, - project_id: project - } - - expect(response).to be_success - end - - it 'projects/ci_cd_settings_with_variables.html' do - create(:ci_variable, project: project_variable_populated) - create(:ci_variable, project: project_variable_populated) - - get :show, params: { - namespace_id: project_variable_populated.namespace.to_param, - project_id: project_variable_populated - } - - expect(response).to be_success - end - end -end diff --git a/spec/javascripts/fixtures/prometheus_service.rb b/spec/javascripts/fixtures/prometheus_service.rb deleted file mode 100644 index f3171fdd97b..00000000000 --- a/spec/javascripts/fixtures/prometheus_service.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' - -describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } - let!(:service) { create(:prometheus_service, project: project) } - - render_views - - before(:all) do - clean_frontend_fixtures('services/prometheus') - end - - before do - sign_in(admin) - end - - after do - remove_repository(project) - end - - it 'services/prometheus/prometheus_service.html' do - get :edit, params: { - namespace_id: namespace, - project_id: project, - id: service.to_param - } - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/raw.rb b/spec/javascripts/fixtures/raw.rb deleted file mode 100644 index 801c80a0112..00000000000 --- a/spec/javascripts/fixtures/raw.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -describe 'Raw files', '(JavaScript fixtures)' do - include JavaScriptFixturesHelpers - - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'raw-project') } - let(:response) { @blob.data.force_encoding('UTF-8') } - - before(:all) do - clean_frontend_fixtures('blob/balsamiq/') - clean_frontend_fixtures('blob/notebook/') - clean_frontend_fixtures('blob/pdf/') - end - - after do - remove_repository(project) - end - - it 'blob/balsamiq/test.bmpr' do - @blob = project.repository.blob_at('b89b56d79', 'files/images/balsamiq.bmpr') - end - - it 'blob/notebook/basic.json' do - @blob = project.repository.blob_at('6d85bb69', 'files/ipython/basic.ipynb') - end - - it 'blob/notebook/worksheets.json' do - @blob = project.repository.blob_at('6d85bb69', 'files/ipython/worksheets.ipynb') - end - - it 'blob/notebook/math.json' do - @blob = project.repository.blob_at('93ee732', 'files/ipython/math.ipynb') - end - - it 'blob/pdf/test.pdf' do - @blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf') - end -end diff --git a/spec/javascripts/fixtures/search.rb b/spec/javascripts/fixtures/search.rb deleted file mode 100644 index 22fc546d761..00000000000 --- a/spec/javascripts/fixtures/search.rb +++ /dev/null @@ -1,17 +0,0 @@ -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' do - get :show - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb deleted file mode 100644 index 2237702ccca..00000000000 --- a/spec/javascripts/fixtures/services.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' - -describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } - let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') } - - render_views - - before(:all) do - clean_frontend_fixtures('services/') - end - - before do - sign_in(admin) - end - - after do - remove_repository(project) - end - - it 'services/edit_service.html' do - get :edit, params: { - namespace_id: namespace, - project_id: project, - id: service.to_param - } - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/sessions.rb b/spec/javascripts/fixtures/sessions.rb deleted file mode 100644 index 92b74c01c89..00000000000 --- a/spec/javascripts/fixtures/sessions.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'spec_helper' - -describe 'Sessions (JavaScript fixtures)' do - include JavaScriptFixturesHelpers - - before(:all) do - clean_frontend_fixtures('sessions/') - end - - describe SessionsController, '(JavaScript fixtures)', type: :controller do - include DeviseHelpers - - render_views - - before do - set_devise_mapping(context: @request) - end - - it 'sessions/new.html' do - get :new - - expect(response).to be_success - end - end -end diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb deleted file mode 100644 index ace84b14eb7..00000000000 --- a/spec/javascripts/fixtures/snippet.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' - -describe SnippetsController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } - let(:snippet) { create(:personal_snippet, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: admin) } - - render_views - - before(:all) do - clean_frontend_fixtures('snippets/') - end - - before do - sign_in(admin) - allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo']) - end - - after do - remove_repository(project) - end - - it 'snippets/show.html' do - create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') - - get(:show, params: { id: snippet.to_param }) - - expect(response).to be_success - end -end diff --git a/spec/javascripts/fixtures/static/README.md b/spec/javascripts/fixtures/static/README.md deleted file mode 100644 index b5c2f8233bf..00000000000 --- a/spec/javascripts/fixtures/static/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not add new files here! - -Instead use a Ruby file in the fixtures root directory (`spec/javascripts/fixtures/`). diff --git a/spec/javascripts/fixtures/static/ajax_loading_spinner.html b/spec/javascripts/fixtures/static/ajax_loading_spinner.html deleted file mode 100644 index 0e1ebb32b1c..00000000000 --- a/spec/javascripts/fixtures/static/ajax_loading_spinner.html +++ /dev/null @@ -1,3 +0,0 @@ -<a class="js-ajax-loading-spinner" data-remote href="http://goesnowhere.nothing/whereami"> -<i class="fa fa-trash-o"></i> -</a> diff --git a/spec/javascripts/fixtures/static/balsamiq_viewer.html b/spec/javascripts/fixtures/static/balsamiq_viewer.html deleted file mode 100644 index cdd723d1a84..00000000000 --- a/spec/javascripts/fixtures/static/balsamiq_viewer.html +++ /dev/null @@ -1 +0,0 @@ -<div class="file-content balsamiq-viewer" data-endpoint="/test" id="js-balsamiq-viewer"></div> diff --git a/spec/javascripts/fixtures/static/create_item_dropdown.html b/spec/javascripts/fixtures/static/create_item_dropdown.html deleted file mode 100644 index d2d38370092..00000000000 --- a/spec/javascripts/fixtures/static/create_item_dropdown.html +++ /dev/null @@ -1,11 +0,0 @@ -<div class="js-create-item-dropdown-fixture-root"> -<input name="variable[environment]" type="hidden"> -<div class="dropdown "><button class="dropdown-menu-toggle js-dropdown-menu-toggle" type="button" data-toggle="dropdown"><span class="dropdown-toggle-text ">some label</span><i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i></button><div class="dropdown-menu dropdown-select dropdown-menu-selectable"><div class="dropdown-input"><input type="search" id="" class="dropdown-input-field" autocomplete="off" /><i aria-hidden="true" data-hidden="true" class="fa fa-search dropdown-input-search"></i><i aria-hidden="true" data-hidden="true" role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i></div><div class="dropdown-content js-dropdown-content"></div><div class="dropdown-footer"><ul class="dropdown-footer-list"> -<li> -<button class="dropdown-create-new-item-button js-dropdown-create-new-item"> -Create wildcard -<code></code> -</button> -</li> -</ul> -</div><div class="dropdown-loading"><i aria-hidden="true" data-hidden="true" class="fa fa-spinner fa-spin"></i></div></div></div></div> diff --git a/spec/javascripts/fixtures/static/environments/table.html b/spec/javascripts/fixtures/static/environments/table.html deleted file mode 100644 index 417af564ff1..00000000000 --- a/spec/javascripts/fixtures/static/environments/table.html +++ /dev/null @@ -1,15 +0,0 @@ -<table> -<thead> -<tr> -<th>Environment</th> -<th>Last deployment</th> -<th>Job</th> -<th>Commit</th> -<th></th> -<th></th> -</tr> -</thead> -<tbody> -<tr id="environment-row"></tr> -</tbody> -</table> diff --git a/spec/javascripts/fixtures/static/event_filter.html b/spec/javascripts/fixtures/static/event_filter.html deleted file mode 100644 index 8e9b6fb1b5c..00000000000 --- a/spec/javascripts/fixtures/static/event_filter.html +++ /dev/null @@ -1,44 +0,0 @@ -<ul class="nav-links event-filter scrolling-tabs nav nav-tabs"> -<li class="active"> -<a class="event-filter-link" href="/dashboard/activity" id="all_event_filter" title="Filter by all"> -<span> -All -</span> -</a> -</li> -<li> -<a class="event-filter-link" href="/dashboard/activity" id="push_event_filter" title="Filter by push events"> -<span> -Push events -</span> -</a> -</li> -<li> -<a class="event-filter-link" href="/dashboard/activity" id="merged_event_filter" title="Filter by merge events"> -<span> -Merge events -</span> -</a> -</li> -<li> -<a class="event-filter-link" href="/dashboard/activity" id="issue_event_filter" title="Filter by issue events"> -<span> -Issue events -</span> -</a> -</li> -<li> -<a class="event-filter-link" href="/dashboard/activity" id="comments_event_filter" title="Filter by comments"> -<span> -Comments -</span> -</a> -</li> -<li> -<a class="event-filter-link" href="/dashboard/activity" id="team_event_filter" title="Filter by team"> -<span> -Team -</span> -</a> -</li> -</ul> diff --git a/spec/javascripts/fixtures/static/gl_dropdown.html b/spec/javascripts/fixtures/static/gl_dropdown.html deleted file mode 100644 index 08f6738414e..00000000000 --- a/spec/javascripts/fixtures/static/gl_dropdown.html +++ /dev/null @@ -1,26 +0,0 @@ -<div> -<div class="dropdown inline"> -<button class="dropdown-menu-toggle" data-toggle="dropdown" id="js-project-dropdown" type="button"> -<div class="dropdown-toggle-text"> -Projects -</div> -<i class="fa fa-chevron-down dropdown-toggle-caret js-projects-dropdown-toggle"></i> -</button> -<div class="dropdown-menu dropdown-select dropdown-menu-selectable"> -<div class="dropdown-title"> -<span>Go to project</span> -<button aria="{:label=>"Close"}" class="dropdown-title-button dropdown-menu-close"> -<i class="fa fa-times dropdown-menu-close-icon"></i> -</button> -</div> -<div class="dropdown-input"> -<input class="dropdown-input-field" placeholder="Filter results" type="search"> -<i class="fa fa-search dropdown-input-search"></i> -</div> -<div class="dropdown-content"></div> -<div class="dropdown-loading"> -<i class="fa fa-spinner fa-spin"></i> -</div> -</div> -</div> -</div> diff --git a/spec/javascripts/fixtures/static/gl_field_errors.html b/spec/javascripts/fixtures/static/gl_field_errors.html deleted file mode 100644 index f8470e02b7c..00000000000 --- a/spec/javascripts/fixtures/static/gl_field_errors.html +++ /dev/null @@ -1,22 +0,0 @@ -<form action="submit" class="gl-show-field-errors" method="post"> -<div class="form-group"> -<input class="required-text" required type="text">Text</input> -</div> -<div class="form-group"> -<input class="email" required title="Please provide a valid email address." type="email">Email</input> -</div> -<div class="form-group"> -<input class="password" required type="password">Password</input> -</div> -<div class="form-group"> -<input class="alphanumeric" pattern="[a-zA-Z0-9]" required type="text">Alphanumeric</input> -</div> -<div class="form-group"> -<input class="hidden" type="hidden"> -</div> -<div class="form-group"> -<input class="custom gl-field-error-ignore" type="text">Custom, do not validate</input> -</div> -<div class="form-group"></div> -<input class="submit" type="submit">Submit</input> -</form> diff --git a/spec/javascripts/fixtures/static/images/green_box.png b/spec/javascripts/fixtures/static/images/green_box.png Binary files differdeleted file mode 100644 index cd1ff9f9ade..00000000000 --- a/spec/javascripts/fixtures/static/images/green_box.png +++ /dev/null diff --git a/spec/javascripts/fixtures/static/images/one_white_pixel.png b/spec/javascripts/fixtures/static/images/one_white_pixel.png Binary files differdeleted file mode 100644 index 073fcf40a18..00000000000 --- a/spec/javascripts/fixtures/static/images/one_white_pixel.png +++ /dev/null diff --git a/spec/javascripts/fixtures/static/images/red_box.png b/spec/javascripts/fixtures/static/images/red_box.png Binary files differdeleted file mode 100644 index 73b2927da0f..00000000000 --- a/spec/javascripts/fixtures/static/images/red_box.png +++ /dev/null diff --git a/spec/javascripts/fixtures/static/issuable_filter.html b/spec/javascripts/fixtures/static/issuable_filter.html deleted file mode 100644 index 06b70fb43f1..00000000000 --- a/spec/javascripts/fixtures/static/issuable_filter.html +++ /dev/null @@ -1,9 +0,0 @@ -<form action="/user/project/issues?scope=all&state=closed" class="js-filter-form"> -<input id="utf8" name="utf8" value="✓"> -<input id="check-all-issues" name="check-all-issues"> -<input id="search" name="search"> -<input id="author_id" name="author_id"> -<input id="assignee_id" name="assignee_id"> -<input id="milestone_title" name="milestone_title"> -<input id="label_name" name="label_name"> -</form> diff --git a/spec/javascripts/fixtures/static/issue_sidebar_label.html b/spec/javascripts/fixtures/static/issue_sidebar_label.html deleted file mode 100644 index ec8fb30f219..00000000000 --- a/spec/javascripts/fixtures/static/issue_sidebar_label.html +++ /dev/null @@ -1,26 +0,0 @@ -<div class="block labels"> -<div class="sidebar-collapsed-icon js-sidebar-labels-tooltip"></div> -<div class="title hide-collapsed"> -<a class="edit-link float-right" href="#"> -Edit -</a> -</div> -<div class="selectbox hide-collapsed" style="display: none;"> -<div class="dropdown"> -<button class="dropdown-menu-toggle js-label-select js-multiselect" data-ability-name="issue" data-field-name="issue[label_names][]" data-issue-update="/root/test/issues/2.json" data-labels="/root/test/labels.json" data-project-id="12" data-show-any="true" data-show-no="true" data-toggle="dropdown" type="button"> -<span class="dropdown-toggle-text"> -Label -</span> -<i class="fa fa-chevron-down"></i> -</button> -<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"> -<div class="dropdown-page-one"> -<div class="dropdown-content"></div> -<div class="dropdown-loading"> -<i class="fa fa-spinner fa-spin"></i> -</div> -</div> -</div> -</div> -</div> -</div> diff --git a/spec/javascripts/fixtures/static/line_highlighter.html b/spec/javascripts/fixtures/static/line_highlighter.html deleted file mode 100644 index 897a25d6760..00000000000 --- a/spec/javascripts/fixtures/static/line_highlighter.html +++ /dev/null @@ -1,107 +0,0 @@ -<div class="file-holder"> -<div class="file-content"> -<div class="line-numbers"> -<a data-line-number="1" href="#L1" id="L1"> -<i class="fa fa-link"></i> -1 -</a> -<a data-line-number="2" href="#L2" id="L2"> -<i class="fa fa-link"></i> -2 -</a> -<a data-line-number="3" href="#L3" id="L3"> -<i class="fa fa-link"></i> -3 -</a> -<a data-line-number="4" href="#L4" id="L4"> -<i class="fa fa-link"></i> -4 -</a> -<a data-line-number="5" href="#L5" id="L5"> -<i class="fa fa-link"></i> -5 -</a> -<a data-line-number="6" href="#L6" id="L6"> -<i class="fa fa-link"></i> -6 -</a> -<a data-line-number="7" href="#L7" id="L7"> -<i class="fa fa-link"></i> -7 -</a> -<a data-line-number="8" href="#L8" id="L8"> -<i class="fa fa-link"></i> -8 -</a> -<a data-line-number="9" href="#L9" id="L9"> -<i class="fa fa-link"></i> -9 -</a> -<a data-line-number="10" href="#L10" id="L10"> -<i class="fa fa-link"></i> -10 -</a> -<a data-line-number="11" href="#L11" id="L11"> -<i class="fa fa-link"></i> -11 -</a> -<a data-line-number="12" href="#L12" id="L12"> -<i class="fa fa-link"></i> -12 -</a> -<a data-line-number="13" href="#L13" id="L13"> -<i class="fa fa-link"></i> -13 -</a> -<a data-line-number="14" href="#L14" id="L14"> -<i class="fa fa-link"></i> -14 -</a> -<a data-line-number="15" href="#L15" id="L15"> -<i class="fa fa-link"></i> -15 -</a> -<a data-line-number="16" href="#L16" id="L16"> -<i class="fa fa-link"></i> -16 -</a> -<a data-line-number="17" href="#L17" id="L17"> -<i class="fa fa-link"></i> -17 -</a> -<a data-line-number="18" href="#L18" id="L18"> -<i class="fa fa-link"></i> -18 -</a> -<a data-line-number="19" href="#L19" id="L19"> -<i class="fa fa-link"></i> -19 -</a> -<a data-line-number="20" href="#L20" id="L20"> -<i class="fa fa-link"></i> -20 -</a> -<a data-line-number="21" href="#L21" id="L21"> -<i class="fa fa-link"></i> -21 -</a> -<a data-line-number="22" href="#L22" id="L22"> -<i class="fa fa-link"></i> -22 -</a> -<a data-line-number="23" href="#L23" id="L23"> -<i class="fa fa-link"></i> -23 -</a> -<a data-line-number="24" href="#L24" id="L24"> -<i class="fa fa-link"></i> -24 -</a> -<a data-line-number="25" href="#L25" id="L25"> -<i class="fa fa-link"></i> -25 -</a> -</div> -<pre class="code highlight"><code><span class="line" id="LC1">Line 1</span><span class="line" id="LC2">Line 2</span><span class="line" id="LC3">Line 3</span><span class="line" id="LC4">Line 4</span><span class="line" id="LC5">Line 5</span><span class="line" id="LC6">Line 6</span><span class="line" id="LC7">Line 7</span><span class="line" id="LC8">Line 8</span><span class="line" id="LC9">Line 9</span><span class="line" id="LC10">Line 10</span><span class="line" id="LC11">Line 11</span><span class="line" id="LC12">Line 12</span><span class="line" id="LC13">Line 13</span><span class="line" id="LC14">Line 14</span><span class="line" id="LC15">Line 15</span><span class="line" id="LC16">Line 16</span><span class="line" id="LC17">Line 17</span><span class="line" id="LC18">Line 18</span><span class="line" id="LC19">Line 19</span><span class="line" id="LC20">Line 20</span><span class="line" id="LC21">Line 21</span><span class="line" id="LC22">Line 22</span><span class="line" id="LC23">Line 23</span><span class="line" id="LC24">Line 24</span><span class="line" id="LC25">Line 25</span></code></pre> -</div> -</div> diff --git a/spec/javascripts/fixtures/static/linked_tabs.html b/spec/javascripts/fixtures/static/linked_tabs.html deleted file mode 100644 index c25463bf1db..00000000000 --- a/spec/javascripts/fixtures/static/linked_tabs.html +++ /dev/null @@ -1,20 +0,0 @@ -<ul class="nav nav-tabs new-session-tabs linked-tabs"> -<li class="nav-item"> -<a class="nav-link" data-action="tab1" data-target="div#tab1" data-toggle="tab" href="foo/bar/1"> -Tab 1 -</a> -</li> -<li class="nav-item"> -<a class="nav-link" data-action="tab2" data-target="div#tab2" data-toggle="tab" href="foo/bar/1/context"> -Tab 2 -</a> -</li> -</ul> -<div class="tab-content"> -<div class="tab-pane" id="tab1"> -Tab 1 Content -</div> -<div class="tab-pane" id="tab2"> -Tab 2 Content -</div> -</div> diff --git a/spec/javascripts/fixtures/static/merge_requests_show.html b/spec/javascripts/fixtures/static/merge_requests_show.html deleted file mode 100644 index 87e36c9f315..00000000000 --- a/spec/javascripts/fixtures/static/merge_requests_show.html +++ /dev/null @@ -1,15 +0,0 @@ -<a class="btn-close"></a> -<div class="detail-page-description"> -<div class="description js-task-list-container"> -<div class="md"> -<ul class="task-list"> -<li class="task-list-item"> -<input class="task-list-item-checkbox" type="checkbox"> -Task List Item -</li> -</ul> -<textarea class="js-task-list-field">- [ ] Task List Item</textarea> -</div> -</div> -</div> -<form action="/foo" class="js-issuable-update"></form> diff --git a/spec/javascripts/fixtures/static/mini_dropdown_graph.html b/spec/javascripts/fixtures/static/mini_dropdown_graph.html deleted file mode 100644 index cd0b8dec3fc..00000000000 --- a/spec/javascripts/fixtures/static/mini_dropdown_graph.html +++ /dev/null @@ -1,13 +0,0 @@ -<div class="js-builds-dropdown-tests dropdown dropdown js-mini-pipeline-graph"> -<button class="js-builds-dropdown-button" data-toggle="dropdown" data-stage-endpoint="foobar"> -Dropdown -</button> -<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> -<li class="js-builds-dropdown-list scrollable-menu"> -<ul></ul> -</li> -<li class="js-builds-dropdown-loading hidden"> -<span class="fa fa-spinner"></span> -</li> -</ul> -</div> diff --git a/spec/javascripts/fixtures/static/notebook_viewer.html b/spec/javascripts/fixtures/static/notebook_viewer.html deleted file mode 100644 index 4bbb7bf1094..00000000000 --- a/spec/javascripts/fixtures/static/notebook_viewer.html +++ /dev/null @@ -1 +0,0 @@ -<div class="file-content" data-endpoint="/test" id="js-notebook-viewer"></div> diff --git a/spec/javascripts/fixtures/static/oauth_remember_me.html b/spec/javascripts/fixtures/static/oauth_remember_me.html deleted file mode 100644 index 9ba1ffc72fe..00000000000 --- a/spec/javascripts/fixtures/static/oauth_remember_me.html +++ /dev/null @@ -1,6 +0,0 @@ -<div id="oauth-container"> -<input id="remember_me" type="checkbox"> -<a class="oauth-login twitter" href="http://example.com/"></a> -<a class="oauth-login github" href="http://example.com/"></a> -<a class="oauth-login facebook" href="http://example.com/?redirect_fragment=L1"></a> -</div> diff --git a/spec/javascripts/fixtures/static/pdf_viewer.html b/spec/javascripts/fixtures/static/pdf_viewer.html deleted file mode 100644 index 350d35a262f..00000000000 --- a/spec/javascripts/fixtures/static/pdf_viewer.html +++ /dev/null @@ -1 +0,0 @@ -<div class="file-content" data-endpoint="/test" id="js-pdf-viewer"></div> diff --git a/spec/javascripts/fixtures/static/pipeline_graph.html b/spec/javascripts/fixtures/static/pipeline_graph.html deleted file mode 100644 index 422372bb7d5..00000000000 --- a/spec/javascripts/fixtures/static/pipeline_graph.html +++ /dev/null @@ -1,24 +0,0 @@ -<div class="pipeline-visualization js-pipeline-graph"> -<ul class="stage-column-list"> -<li class="stage-column"> -<div class="stage-name"> -<a href="/"> -Test -<div class="builds-container"> -<ul> -<li class="build"> -<div class="curve"></div> -<a> -<svg></svg> -<div class="ci-status-text"> -stop_review -</div> -</a> -</li> -</ul> -</div> -</a> -</div> -</li> -</ul> -</div> diff --git a/spec/javascripts/fixtures/static/pipelines.html b/spec/javascripts/fixtures/static/pipelines.html deleted file mode 100644 index 42333f94f2f..00000000000 --- a/spec/javascripts/fixtures/static/pipelines.html +++ /dev/null @@ -1,3 +0,0 @@ -<div> -<div data-can-create-pipeline="true" data-ci-lint-path="foo" data-empty-state-svg-path="foo" data-endpoint="foo" data-error-state-svg-path="foo" data-has-ci="foo" data-help-auto-devops-path="foo" data-help-page-path="foo" data-new-pipeline-path="foo" data-reset-cache-path="foo" id="pipelines-list-vue"></div> -</div> diff --git a/spec/javascripts/fixtures/static/project_select_combo_button.html b/spec/javascripts/fixtures/static/project_select_combo_button.html deleted file mode 100644 index 50c826051c0..00000000000 --- a/spec/javascripts/fixtures/static/project_select_combo_button.html +++ /dev/null @@ -1,9 +0,0 @@ -<div class="project-item-select-holder"> -<input class="project-item-select" data-group-id="12345" data-relative-path="issues/new"> -<a class="new-project-item-link" data-label="New issue" data-type="issues" href=""> -<i class="fa fa-spinner spin"></i> -</a> -<a class="new-project-item-select-button"> -<i class="fa fa-caret-down"></i> -</a> -</div> diff --git a/spec/javascripts/fixtures/static/projects.json b/spec/javascripts/fixtures/static/projects.json deleted file mode 100644 index 68a150f602a..00000000000 --- a/spec/javascripts/fixtures/static/projects.json +++ /dev/null @@ -1,445 +0,0 @@ -[{ - "id": 9, - "description": "", - "default_branch": null, - "tag_list": [], - "public": true, - "archived": false, - "visibility_level": 20, - "ssh_url_to_repo": "phil@localhost:root/test.git", - "http_url_to_repo": "http://localhost:3000/root/test.git", - "web_url": "http://localhost:3000/root/test", - "owner": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", - "web_url": "http://localhost:3000/u/root" - }, - "name": "test", - "name_with_namespace": "Administrator / test", - "path": "test", - "path_with_namespace": "root/test", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "created_at": "2016-01-14T19:08:05.364Z", - "last_activity_at": "2016-01-14T19:08:07.418Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 1, - "name": "root", - "path": "root", - "owner_id": 1, - "created_at": "2016-01-13T20:19:44.439Z", - "updated_at": "2016-01-13T20:19:44.439Z", - "description": "", - "avatar": null - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "only_allow_merge_if_pipeline_succeeds": false, - "open_issues_count": 0, - "permissions": { - "project_access": null, - "group_access": null - } -}, { - "id": 8, - "description": "Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 0, - "ssh_url_to_repo": "phil@localhost:h5bp/html5-boilerplate.git", - "http_url_to_repo": "http://localhost:3000/h5bp/html5-boilerplate.git", - "web_url": "http://localhost:3000/h5bp/html5-boilerplate", - "name": "Html5 Boilerplate", - "name_with_namespace": "H5bp / Html5 Boilerplate", - "path": "html5-boilerplate", - "path_with_namespace": "h5bp/html5-boilerplate", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "created_at": "2016-01-13T20:19:57.525Z", - "last_activity_at": "2016-01-13T20:27:57.280Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 5, - "name": "H5bp", - "path": "h5bp", - "owner_id": null, - "created_at": "2016-01-13T20:19:57.239Z", - "updated_at": "2016-01-13T20:19:57.239Z", - "description": "Tempore accusantium possimus aut libero.", - "avatar": { - "url": null - } - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "only_allow_merge_if_pipeline_succeeds": false, - "open_issues_count": 5, - "permissions": { - "project_access": { - "access_level": 10, - "notification_level": 3 - }, - "group_access": { - "access_level": 50, - "notification_level": 3 - } - } -}, { - "id": 7, - "description": "Modi odio mollitia dolorem qui.", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 0, - "ssh_url_to_repo": "phil@localhost:twitter/typeahead-js.git", - "http_url_to_repo": "http://localhost:3000/twitter/typeahead-js.git", - "web_url": "http://localhost:3000/twitter/typeahead-js", - "name": "Typeahead.Js", - "name_with_namespace": "Twitter / Typeahead.Js", - "path": "typeahead-js", - "path_with_namespace": "twitter/typeahead-js", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "created_at": "2016-01-13T20:19:56.212Z", - "last_activity_at": "2016-01-13T20:27:51.496Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 4, - "name": "Twitter", - "path": "twitter", - "owner_id": null, - "created_at": "2016-01-13T20:19:54.480Z", - "updated_at": "2016-01-13T20:19:54.480Z", - "description": "Id voluptatem ipsa maiores omnis repudiandae et et.", - "avatar": { - "url": null - } - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "only_allow_merge_if_pipeline_succeeds": true, - "open_issues_count": 4, - "permissions": { - "project_access": null, - "group_access": { - "access_level": 10, - "notification_level": 3 - } - } -}, { - "id": 6, - "description": "Omnis asperiores ipsa et beatae quidem necessitatibus quia.", - "default_branch": "master", - "tag_list": [], - "public": true, - "archived": false, - "visibility_level": 20, - "ssh_url_to_repo": "phil@localhost:twitter/flight.git", - "http_url_to_repo": "http://localhost:3000/twitter/flight.git", - "web_url": "http://localhost:3000/twitter/flight", - "name": "Flight", - "name_with_namespace": "Twitter / Flight", - "path": "flight", - "path_with_namespace": "twitter/flight", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "created_at": "2016-01-13T20:19:54.754Z", - "last_activity_at": "2016-01-13T20:27:50.502Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 4, - "name": "Twitter", - "path": "twitter", - "owner_id": null, - "created_at": "2016-01-13T20:19:54.480Z", - "updated_at": "2016-01-13T20:19:54.480Z", - "description": "Id voluptatem ipsa maiores omnis repudiandae et et.", - "avatar": { - "url": null - } - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "only_allow_merge_if_pipeline_succeeds": true, - "open_issues_count": 4, - "permissions": { - "project_access": null, - "group_access": { - "access_level": 10, - "notification_level": 3 - } - } -}, { - "id": 5, - "description": "Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 0, - "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-test.git", - "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-test.git", - "web_url": "http://localhost:3000/gitlab-org/gitlab-test", - "name": "Gitlab Test", - "name_with_namespace": "Gitlab Org / Gitlab Test", - "path": "gitlab-test", - "path_with_namespace": "gitlab-org/gitlab-test", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "created_at": "2016-01-13T20:19:53.202Z", - "last_activity_at": "2016-01-13T20:27:41.626Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 3, - "name": "Gitlab Org", - "path": "gitlab-org", - "owner_id": null, - "created_at": "2016-01-13T20:19:48.851Z", - "updated_at": "2016-01-13T20:19:48.851Z", - "description": "Magni mollitia quod quidem soluta nesciunt impedit.", - "avatar": { - "url": null - } - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "only_allow_merge_if_pipeline_succeeds": false, - "open_issues_count": 5, - "permissions": { - "project_access": null, - "group_access": { - "access_level": 50, - "notification_level": 3 - } - } -}, { - "id": 4, - "description": "Aut molestias quas est ut aperiam officia quod libero.", - "default_branch": "master", - "tag_list": [], - "public": true, - "archived": false, - "visibility_level": 20, - "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-shell.git", - "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-shell.git", - "web_url": "http://localhost:3000/gitlab-org/gitlab-shell", - "name": "Gitlab Shell", - "name_with_namespace": "Gitlab Org / Gitlab Shell", - "path": "gitlab-shell", - "path_with_namespace": "gitlab-org/gitlab-shell", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "created_at": "2016-01-13T20:19:51.882Z", - "last_activity_at": "2016-01-13T20:27:35.678Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 3, - "name": "Gitlab Org", - "path": "gitlab-org", - "owner_id": null, - "created_at": "2016-01-13T20:19:48.851Z", - "updated_at": "2016-01-13T20:19:48.851Z", - "description": "Magni mollitia quod quidem soluta nesciunt impedit.", - "avatar": { - "url": null - } - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "only_allow_merge_if_pipeline_succeeds": false, - "open_issues_count": 5, - "permissions": { - "project_access": { - "access_level": 20, - "notification_level": 3 - }, - "group_access": { - "access_level": 50, - "notification_level": 3 - } - } -}, { - "id": 3, - "description": "Excepturi molestiae quia repellendus omnis est illo illum eligendi.", - "default_branch": "master", - "tag_list": [], - "public": true, - "archived": false, - "visibility_level": 20, - "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-ci.git", - "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-ci.git", - "web_url": "http://localhost:3000/gitlab-org/gitlab-ci", - "name": "Gitlab Ci", - "name_with_namespace": "Gitlab Org / Gitlab Ci", - "path": "gitlab-ci", - "path_with_namespace": "gitlab-org/gitlab-ci", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "created_at": "2016-01-13T20:19:50.346Z", - "last_activity_at": "2016-01-13T20:27:30.115Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 3, - "name": "Gitlab Org", - "path": "gitlab-org", - "owner_id": null, - "created_at": "2016-01-13T20:19:48.851Z", - "updated_at": "2016-01-13T20:19:48.851Z", - "description": "Magni mollitia quod quidem soluta nesciunt impedit.", - "avatar": { - "url": null - } - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "only_allow_merge_if_pipeline_succeeds": false, - "open_issues_count": 3, - "permissions": { - "project_access": null, - "group_access": { - "access_level": 50, - "notification_level": 3 - } - } -}, { - "id": 2, - "description": "Adipisci quaerat dignissimos enim sed ipsam dolorem quia.", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 10, - "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-ce.git", - "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-ce.git", - "web_url": "http://localhost:3000/gitlab-org/gitlab-ce", - "name": "Gitlab Ce", - "name_with_namespace": "Gitlab Org / Gitlab Ce", - "path": "gitlab-ce", - "path_with_namespace": "gitlab-org/gitlab-ce", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "created_at": "2016-01-13T20:19:49.065Z", - "last_activity_at": "2016-01-13T20:26:58.454Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 3, - "name": "Gitlab Org", - "path": "gitlab-org", - "owner_id": null, - "created_at": "2016-01-13T20:19:48.851Z", - "updated_at": "2016-01-13T20:19:48.851Z", - "description": "Magni mollitia quod quidem soluta nesciunt impedit.", - "avatar": { - "url": null - } - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "only_allow_merge_if_pipeline_succeeds": false, - "open_issues_count": 5, - "permissions": { - "project_access": { - "access_level": 30, - "notification_level": 3 - }, - "group_access": { - "access_level": 50, - "notification_level": 3 - } - } -}, { - "id": 1, - "description": "Vel voluptatem maxime saepe ex quia.", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 0, - "ssh_url_to_repo": "phil@localhost:documentcloud/underscore.git", - "http_url_to_repo": "http://localhost:3000/documentcloud/underscore.git", - "web_url": "http://localhost:3000/documentcloud/underscore", - "name": "Underscore", - "name_with_namespace": "Documentcloud / Underscore", - "path": "underscore", - "path_with_namespace": "documentcloud/underscore", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "created_at": "2016-01-13T20:19:45.862Z", - "last_activity_at": "2016-01-13T20:25:03.106Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 2, - "name": "Documentcloud", - "path": "documentcloud", - "owner_id": null, - "created_at": "2016-01-13T20:19:44.464Z", - "updated_at": "2016-01-13T20:19:44.464Z", - "description": "Aut impedit perferendis fuga et ipsa repellat cupiditate et.", - "avatar": { - "url": null - } - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "only_allow_merge_if_pipeline_succeeds": false, - "open_issues_count": 5, - "permissions": { - "project_access": null, - "group_access": { - "access_level": 50, - "notification_level": 3 - } - } -}] diff --git a/spec/javascripts/fixtures/static/search_autocomplete.html b/spec/javascripts/fixtures/static/search_autocomplete.html deleted file mode 100644 index 29db9020424..00000000000 --- a/spec/javascripts/fixtures/static/search_autocomplete.html +++ /dev/null @@ -1,15 +0,0 @@ -<div class="search search-form"> -<form class="form-inline"> -<div class="search-input-container"> -<div class="search-input-wrap"> -<div class="dropdown"> -<input class="search-input dropdown-menu-toggle" id="search"> -<div class="dropdown-menu dropdown-select"> -<div class="dropdown-content"></div> -</div> -</div> -</div> -</div> -<input class="js-search-project-options" type="hidden"> -</form> -</div> diff --git a/spec/javascripts/fixtures/static/signin_tabs.html b/spec/javascripts/fixtures/static/signin_tabs.html deleted file mode 100644 index 7e66ab9394b..00000000000 --- a/spec/javascripts/fixtures/static/signin_tabs.html +++ /dev/null @@ -1,8 +0,0 @@ -<ul class="nav-links new-session-tabs"> -<li class="active"> -<a href="#ldap">LDAP</a> -</li> -<li> -<a href="#login-pane">Standard</a> -</li> -</ul> diff --git a/spec/javascripts/fixtures/static/sketch_viewer.html b/spec/javascripts/fixtures/static/sketch_viewer.html deleted file mode 100644 index e25e554e568..00000000000 --- a/spec/javascripts/fixtures/static/sketch_viewer.html +++ /dev/null @@ -1,3 +0,0 @@ -<div class="file-content" data-endpoint="/test_sketch_file.sketch" id="js-sketch-viewer"> -<div class="js-loading-icon"></div> -</div> diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb deleted file mode 100644 index d0c8a6eca01..00000000000 --- a/spec/javascripts/fixtures/todos.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'spec_helper' - -describe 'Todos (JavaScript fixtures)' do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } - let(:issue_1) { create(:issue, title: 'issue_1', project: project) } - let!(:todo_1) { create(:todo, user: admin, project: project, target: issue_1, created_at: 5.hours.ago) } - let(:issue_2) { create(:issue, title: 'issue_2', project: project) } - let!(:todo_2) { create(:todo, :done, user: admin, project: project, target: issue_2, created_at: 50.hours.ago) } - - before(:all) do - clean_frontend_fixtures('todos/') - end - - after do - remove_repository(project) - end - - describe Dashboard::TodosController, '(JavaScript fixtures)', type: :controller do - render_views - - before do - sign_in(admin) - end - - it 'todos/todos.html' do - get :index - - expect(response).to be_success - end - end - - describe Projects::TodosController, '(JavaScript fixtures)', type: :controller do - render_views - - before do - sign_in(admin) - end - - it 'todos/todos.json' do - post :create, params: { - namespace_id: namespace, - project_id: project, - issuable_type: 'issue', - issuable_id: issue_2.id - }, format: 'json' - - expect(response).to be_success - end - end -end diff --git a/spec/javascripts/fixtures/u2f.rb b/spec/javascripts/fixtures/u2f.rb deleted file mode 100644 index f52832b6efb..00000000000 --- a/spec/javascripts/fixtures/u2f.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'spec_helper' - -context 'U2F' do - include JavaScriptFixturesHelpers - - let(:user) { create(:user, :two_factor_via_u2f, otp_secret: 'otpsecret:coolkids') } - - before(:all) do - clean_frontend_fixtures('u2f/') - end - - describe SessionsController, '(JavaScript fixtures)', type: :controller do - include DeviseHelpers - - render_views - - before do - set_devise_mapping(context: @request) - end - - it 'u2f/authenticate.html' do - allow(controller).to receive(:find_user).and_return(user) - - post :create, params: { user: { login: user.username, password: user.password } } - - expect(response).to be_success - end - end - - describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do - render_views - - before do - sign_in(user) - allow_any_instance_of(Profiles::TwoFactorAuthsController).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares') - end - - it 'u2f/register.html' do - get :show - - expect(response).to be_success - end - end -end diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js index 4d6d0c895b6..cc88a7ac6c1 100644 --- a/spec/javascripts/groups/components/group_item_spec.js +++ b/spec/javascripts/groups/components/group_item_spec.js @@ -156,6 +156,8 @@ describe('GroupItemComponent', () => { describe('template', () => { it('should render component template correctly', () => { + const visibilityIconEl = vm.$el.querySelector('.item-visibility'); + expect(vm.$el.getAttribute('id')).toBe('group-55'); expect(vm.$el.classList.contains('group-row')).toBeTruthy(); @@ -173,6 +175,11 @@ describe('GroupItemComponent', () => { expect(vm.$el.querySelector('.title')).toBeDefined(); expect(vm.$el.querySelector('.title a.no-expand')).toBeDefined(); + + expect(visibilityIconEl).not.toBe(null); + expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip); + expect(visibilityIconEl.querySelectorAll('svg').length).toBeGreaterThan(0); + expect(vm.$el.querySelector('.access-type')).toBeDefined(); expect(vm.$el.querySelector('.description')).toBeDefined(); diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js index 00d6a4817d7..b2441babf3f 100644 --- a/spec/javascripts/groups/components/item_stats_spec.js +++ b/spec/javascripts/groups/components/item_stats_spec.js @@ -108,18 +108,6 @@ describe('ItemStatsComponent', () => { vm.$destroy(); }); - it('renders item visibility icon and tooltip correctly', () => { - const vm = createComponent(); - - const visibilityIconEl = vm.$el.querySelector('.item-visibility'); - - expect(visibilityIconEl).not.toBe(null); - expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip); - expect(visibilityIconEl.querySelectorAll('svg').length).toBeGreaterThan(0); - - vm.$destroy(); - }); - it('renders start count and last updated information for project item correctly', () => { const item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT, diff --git a/spec/javascripts/helpers/vue_test_utils_helper.js b/spec/javascripts/helpers/vue_test_utils_helper.js index 121e99c9783..5b749b11246 100644 --- a/spec/javascripts/helpers/vue_test_utils_helper.js +++ b/spec/javascripts/helpers/vue_test_utils_helper.js @@ -1,21 +1,5 @@ -/* eslint-disable import/prefer-default-export */ +// No new code should be added to this file. Instead, modify the +// file this one re-exports from. For more detail about why, see: +// https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31349 -const vNodeContainsText = (vnode, text) => - (vnode.text && vnode.text.includes(text)) || - (vnode.children && vnode.children.filter(child => vNodeContainsText(child, text)).length); - -/** - * Determines whether a `shallowMount` Wrapper contains text - * within one of it's slots. This will also work on Wrappers - * acquired with `find()`, but only if it's parent Wrapper - * was shallowMounted. - * NOTE: Prefer checking the rendered output of a component - * wherever possible using something like `text()` instead. - * @param {Wrapper} shallowWrapper - Vue test utils wrapper (shallowMounted) - * @param {String} slotName - * @param {String} text - */ -export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) => - Boolean( - shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length, - ); +export * from '../../frontend/helpers/vue_test_utils_helper'; diff --git a/spec/javascripts/helpers/vue_test_utils_helper_spec.js b/spec/javascripts/helpers/vue_test_utils_helper_spec.js deleted file mode 100644 index 41714066da5..00000000000 --- a/spec/javascripts/helpers/vue_test_utils_helper_spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { shallowWrapperContainsSlotText } from './vue_test_utils_helper'; - -describe('Vue test utils helpers', () => { - describe('shallowWrapperContainsSlotText', () => { - const mockText = 'text'; - const mockSlot = `<div>${mockText}</div>`; - let mockComponent; - - beforeEach(() => { - mockComponent = shallowMount( - { - render(h) { - h(`<div>mockedComponent</div>`); - }, - }, - { - slots: { - default: mockText, - namedSlot: mockSlot, - }, - }, - ); - }); - - it('finds text within shallowWrapper default slot', () => { - expect(shallowWrapperContainsSlotText(mockComponent, 'default', mockText)).toBe(true); - }); - - it('finds text within shallowWrapper named slot', () => { - expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', mockText)).toBe(true); - }); - - it('returns false when text is not present', () => { - const searchText = 'absent'; - - expect(shallowWrapperContainsSlotText(mockComponent, 'default', searchText)).toBe(false); - expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', searchText)).toBe(false); - }); - - it('searches with case-sensitivity', () => { - const searchText = mockText.toUpperCase(); - - expect(shallowWrapperContainsSlotText(mockComponent, 'default', searchText)).toBe(false); - expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', searchText)).toBe(false); - }); - }); -}); diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js index 88652202a8e..c5de31a4138 100644 --- a/spec/javascripts/helpers/vuex_action_helper.js +++ b/spec/javascripts/helpers/vuex_action_helper.js @@ -89,9 +89,7 @@ export default ( payload, ); - return new Promise(resolve => { - setImmediate(resolve); - }) + return new Promise(setImmediate) .then(() => result) .catch(error => { validateResults(); diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js index f63007c7dd2..554bd1ae3b5 100644 --- a/spec/javascripts/ide/components/ide_tree_list_spec.js +++ b/spec/javascripts/ide/components/ide_tree_list_spec.js @@ -58,6 +58,20 @@ describe('IDE tree list', () => { it('renders list of files', () => { expect(vm.$el.textContent).toContain('fileName'); }); + + it('does not render moved entries', done => { + const tree = [file('moved entry'), file('normal entry')]; + tree[0].moved = true; + store.state.trees['abcproject/master'].tree = tree; + const container = vm.$el.querySelector('.ide-tree-body'); + + vm.$nextTick(() => { + expect(container.children.length).toBe(1); + expect(vm.$el.textContent).not.toContain('moved entry'); + expect(vm.$el.textContent).toContain('normal entry'); + done(); + }); + }); }); describe('empty-branch state', () => { diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index 002b5a005b8..0701b773e17 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -5,7 +5,7 @@ import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; import repoEditor from '~/ide/components/repo_editor.vue'; import Editor from '~/ide/lib/editor'; -import { activityBarViews } from '~/ide/constants'; +import { activityBarViews, FILE_VIEW_MODE_EDITOR, FILE_VIEW_MODE_PREVIEW } from '~/ide/constants'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; import { file, resetStore } from '../helpers'; @@ -14,7 +14,10 @@ describe('RepoEditor', () => { let vm; beforeEach(done => { - const f = file(); + const f = { + ...file(), + viewMode: FILE_VIEW_MODE_EDITOR, + }; const RepoEditor = Vue.extend(repoEditor); vm = createComponentWithStore(RepoEditor, store, { @@ -27,6 +30,7 @@ describe('RepoEditor', () => { Vue.set(vm.$store.state.entries, f.path, f); spyOn(vm, 'getFileData').and.returnValue(Promise.resolve()); + spyOn(vm, 'getRawFileData').and.returnValue(Promise.resolve()); vm.$mount(); @@ -41,12 +45,17 @@ describe('RepoEditor', () => { Editor.editorInstance.dispose(); }); - it('renders an ide container', done => { - Vue.nextTick(() => { - expect(vm.shouldHideEditor).toBeFalsy(); + const findEditor = () => vm.$el.querySelector('.multi-file-editor-holder'); + const changeRightPanelCollapsed = () => { + const { state } = vm.$store; - done(); - }); + state.rightPanelCollapsed = !state.rightPanelCollapsed; + }; + + it('renders an ide container', () => { + expect(vm.shouldHideEditor).toBeFalsy(); + expect(vm.showEditor).toBe(true); + expect(findEditor()).not.toHaveCss({ display: 'none' }); }); it('renders only an edit tab', done => { @@ -283,7 +292,7 @@ describe('RepoEditor', () => { }); it('calls updateDimensions when rightPanelCollapsed is changed', done => { - vm.$store.state.rightPanelCollapsed = true; + changeRightPanelCollapsed(); vm.$nextTick(() => { expect(vm.editor.updateDimensions).toHaveBeenCalled(); @@ -358,6 +367,85 @@ describe('RepoEditor', () => { }); }); + describe('when files view mode is preview', () => { + beforeEach(done => { + spyOn(vm.editor, 'updateDimensions'); + vm.file.viewMode = FILE_VIEW_MODE_PREVIEW; + vm.$nextTick(done); + }); + + it('should hide editor', () => { + expect(vm.showEditor).toBe(false); + expect(findEditor()).toHaveCss({ display: 'none' }); + }); + + it('should not update dimensions', done => { + changeRightPanelCollapsed(); + + vm.$nextTick() + .then(() => { + expect(vm.editor.updateDimensions).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + describe('when file view mode changes to editor', () => { + beforeEach(done => { + vm.file.viewMode = FILE_VIEW_MODE_EDITOR; + + // one tick to trigger watch + vm.$nextTick() + // another tick needed until we can update dimensions + .then(() => vm.$nextTick()) + .then(done) + .catch(done.fail); + }); + + it('should update dimensions', () => { + expect(vm.editor.updateDimensions).toHaveBeenCalled(); + }); + }); + }); + + describe('initEditor', () => { + beforeEach(() => { + spyOn(vm.editor, 'createInstance'); + spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); + }); + + it('is being initialised for files without content even if shouldHideEditor is `true`', done => { + vm.file.content = ''; + vm.file.raw = ''; + + vm.initEditor(); + vm.$nextTick() + .then(() => { + expect(vm.getFileData).toHaveBeenCalled(); + expect(vm.getRawFileData).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('does not initialize editor for files already with content', done => { + expect(vm.getFileData.calls.count()).toEqual(1); + expect(vm.getRawFileData.calls.count()).toEqual(1); + + vm.file.content = 'foo'; + + vm.initEditor(); + vm.$nextTick() + .then(() => { + expect(vm.getFileData.calls.count()).toEqual(1); + expect(vm.getRawFileData.calls.count()).toEqual(1); + expect(vm.editor.createInstance).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + it('calls removePendingTab when old file is pending', done => { spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); spyOn(vm, 'removePendingTab'); @@ -367,6 +455,7 @@ describe('RepoEditor', () => { vm.$nextTick() .then(() => { vm.file = file('testing'); + vm.file.content = 'foo'; // need to prevent full cycle of initEditor return vm.$nextTick(); }) diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index dd2313dc800..021c3076094 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -275,6 +275,43 @@ describe('IDE store file actions', () => { }); }); + describe('Re-named success', () => { + beforeEach(() => { + localFile = file(`newCreate-${Math.random()}`); + localFile.url = `project/getFileDataURL`; + localFile.prevPath = 'old-dull-file'; + localFile.path = 'new-shiny-file'; + store.state.entries[localFile.path] = localFile; + + mock.onGet(`${RELATIVE_URL_ROOT}/project/getFileDataURL`).replyOnce( + 200, + { + blame_path: 'blame_path', + commits_path: 'commits_path', + permalink: 'permalink', + raw_path: 'raw_path', + binary: false, + html: '123', + render_error: '', + }, + { + 'page-title': 'testing old-dull-file', + }, + ); + }); + + it('sets document title considering `prevPath` on a file', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(document.title).toBe('testing new-shiny-file'); + + done(); + }) + .catch(done.fail); + }); + }); + describe('error', () => { beforeEach(() => { mock.onGet(`project/getFileDataURL`).networkError(); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index 37354283cab..8504fb3f42b 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -10,6 +10,7 @@ import actions, { deleteEntry, renameEntry, getBranchData, + createTempEntry, } from '~/ide/stores/actions'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; @@ -247,18 +248,30 @@ describe('Multi-file store actions', () => { }); it('sets tmp file as active', done => { - store - .dispatch('createTempEntry', { + testAction( + createTempEntry, + { name: 'test', branchId: 'mybranch', type: 'blob', - }) - .then(f => { - expect(f.active).toBeTruthy(); - - done(); - }) - .catch(done.fail); + }, + store.state, + [ + { type: types.CREATE_TMP_ENTRY, payload: jasmine.any(Object) }, + { type: types.TOGGLE_FILE_OPEN, payload: 'test' }, + { type: types.ADD_FILE_TO_CHANGED, payload: 'test' }, + ], + [ + { + type: 'setFileActive', + payload: 'test', + }, + { + type: 'triggerFilesChange', + }, + ], + done, + ); }); it('creates flash message if file already exists', done => { @@ -488,7 +501,42 @@ describe('Multi-file store actions', () => { 'path', store.state, [{ type: types.DELETE_ENTRY, payload: 'path' }], - [{ type: 'burstUnusedSeal' }, { type: 'triggerFilesChange' }], + [ + { type: 'burstUnusedSeal' }, + { type: 'stageChange', payload: 'path' }, + { type: 'triggerFilesChange' }, + ], + done, + ); + }); + + it('does not delete a folder after it is emptied', done => { + const testFolder = { + type: 'tree', + tree: [], + }; + const testEntry = { + path: 'testFolder/entry-to-delete', + parentPath: 'testFolder', + opened: false, + tree: [], + }; + testFolder.tree.push(testEntry); + store.state.entries = { + testFolder, + 'testFolder/entry-to-delete': testEntry, + }; + + testAction( + deleteEntry, + 'testFolder/entry-to-delete', + store.state, + [{ type: types.DELETE_ENTRY, payload: 'testFolder/entry-to-delete' }], + [ + { type: 'burstUnusedSeal' }, + { type: 'stageChange', payload: 'testFolder/entry-to-delete' }, + { type: 'triggerFilesChange' }, + ], done, ); }); @@ -509,8 +557,15 @@ describe('Multi-file store actions', () => { type: types.RENAME_ENTRY, payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' }, }, + { + type: types.TOGGLE_FILE_CHANGED, + payload: { + file: store.state.entries['parent-path/new-name'], + changed: true, + }, + }, ], - [{ type: 'deleteEntry', payload: 'test' }, { type: 'triggerFilesChange' }], + [{ type: 'triggerFilesChange' }], done, ); }); @@ -557,7 +612,6 @@ describe('Multi-file store actions', () => { parentPath: 'parent-path/new-name', }, }, - { type: 'deleteEntry', payload: 'test' }, { type: 'triggerFilesChange' }, ], done, diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 5f7272311c8..14d861f21d2 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -6,9 +6,11 @@ import eventHub from '~/ide/eventhub'; import consts from '~/ide/stores/modules/commit/constants'; import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types'; import * as actions from '~/ide/stores/modules/commit/actions'; -import testAction from '../../../../helpers/vuex_action_helper'; import { commitActionTypes } from '~/ide/constants'; import { resetStore, file } from 'spec/ide/helpers'; +import testAction from '../../../../helpers/vuex_action_helper'; + +const TEST_COMMIT_SHA = '123456789'; describe('IDE commit module actions', () => { beforeEach(() => { @@ -139,6 +141,9 @@ describe('IDE commit module actions', () => { branches: { master: { workingReference: '', + commit: { + short_id: TEST_COMMIT_SHA, + }, }, }, }; @@ -239,6 +244,9 @@ describe('IDE commit module actions', () => { branches: { master: { workingReference: '1', + commit: { + id: TEST_COMMIT_SHA, + }, }, }, }; @@ -247,7 +255,7 @@ describe('IDE commit module actions', () => { ...file('changed'), type: 'blob', active: true, - lastCommitSha: '123456789', + lastCommitSha: TEST_COMMIT_SHA, }; store.state.stagedFiles.push(f); store.state.changedFiles = [ @@ -307,7 +315,7 @@ describe('IDE commit module actions', () => { previous_path: undefined, }, ], - start_branch: 'master', + start_sha: TEST_COMMIT_SHA, }); done(); @@ -330,11 +338,11 @@ describe('IDE commit module actions', () => { file_path: jasmine.anything(), content: undefined, encoding: jasmine.anything(), - last_commit_id: '123456789', + last_commit_id: TEST_COMMIT_SHA, previous_path: undefined, }, ], - start_branch: undefined, + start_sha: undefined, }); done(); @@ -403,7 +411,7 @@ describe('IDE commit module actions', () => { expect(visitUrl).toHaveBeenCalledWith( `webUrl/merge_requests/new?merge_request[source_branch]=${ store.getters['commit/placeholderBranchName'] - }&merge_request[target_branch]=master`, + }&merge_request[target_branch]=master&nav_source=webide`, ); done(); diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index efd0d86552b..064e66cef64 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -1,5 +1,6 @@ import mutations from '~/ide/stores/mutations/file'; import state from '~/ide/stores/state'; +import { FILE_VIEW_MODE_PREVIEW } from '~/ide/constants'; import { file } from '../../helpers'; describe('IDE store file mutations', () => { @@ -83,6 +84,63 @@ describe('IDE store file mutations', () => { expect(localFile.raw).toBeNull(); expect(localFile.baseRaw).toBeNull(); }); + + it('sets extra file data to all arrays concerned', () => { + localState.stagedFiles = [localFile]; + localState.changedFiles = [localFile]; + localState.openFiles = [localFile]; + + const rawPath = 'foo/bar/blah.md'; + + mutations.SET_FILE_DATA(localState, { + data: { + raw_path: rawPath, + }, + file: localFile, + }); + + expect(localState.stagedFiles[0].rawPath).toEqual(rawPath); + expect(localState.changedFiles[0].rawPath).toEqual(rawPath); + expect(localState.openFiles[0].rawPath).toEqual(rawPath); + expect(localFile.rawPath).toEqual(rawPath); + }); + + it('does not mutate certain props on the file', () => { + const path = 'New Path'; + const name = 'New Name'; + localFile.path = path; + localFile.name = name; + + localState.stagedFiles = [localFile]; + localState.changedFiles = [localFile]; + localState.openFiles = [localFile]; + + mutations.SET_FILE_DATA(localState, { + data: { + path: 'Old Path', + name: 'Old Name', + raw: 'Old Raw', + base_raw: 'Old Base Raw', + }, + file: localFile, + }); + + [ + localState.stagedFiles[0], + localState.changedFiles[0], + localState.openFiles[0], + localFile, + ].forEach(f => { + expect(f).toEqual( + jasmine.objectContaining({ + path, + name, + raw: null, + baseRaw: null, + }), + ); + }); + }); }); describe('SET_FILE_RAW_DATA', () => { @@ -315,6 +373,19 @@ describe('IDE store file mutations', () => { expect(localState.stagedFiles.length).toBe(1); expect(localState.stagedFiles[0].raw).toEqual('testing 123'); }); + + it('adds already-staged file to `replacedFiles`', () => { + localFile.raw = 'already-staged'; + + mutations.STAGE_CHANGE(localState, localFile.path); + + localFile.raw = 'testing 123'; + + mutations.STAGE_CHANGE(localState, localFile.path); + + expect(localState.replacedFiles.length).toBe(1); + expect(localState.replacedFiles[0].raw).toEqual('already-staged'); + }); }); describe('UNSTAGE_CHANGE', () => { @@ -355,10 +426,10 @@ describe('IDE store file mutations', () => { it('updates file view mode', () => { mutations.SET_FILE_VIEWMODE(localState, { file: localFile, - viewMode: 'preview', + viewMode: FILE_VIEW_MODE_PREVIEW, }); - expect(localFile.viewMode).toBe('preview'); + expect(localFile.viewMode).toBe(FILE_VIEW_MODE_PREVIEW); }); }); diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 5ee098bf17f..2470c99e300 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -79,6 +79,16 @@ describe('Multi-file store mutations', () => { }); }); + describe('CLEAR_REPLACED_FILES', () => { + it('clears replacedFiles array', () => { + localState.replacedFiles.push('a'); + + mutations.CLEAR_REPLACED_FILES(localState); + + expect(localState.replacedFiles.length).toBe(0); + }); + }); + describe('UPDATE_VIEWER', () => { it('sets viewer state', () => { mutations.UPDATE_VIEWER(localState, 'diff'); @@ -109,6 +119,62 @@ describe('Multi-file store mutations', () => { }); }); + describe('CREATE_TMP_ENTRY', () => { + beforeEach(() => { + localState.currentProjectId = 'gitlab-ce'; + localState.currentBranchId = 'master'; + localState.trees['gitlab-ce/master'] = { + tree: [], + }; + }); + + it('creates temp entry in the tree', () => { + const tmpFile = file('test'); + mutations.CREATE_TMP_ENTRY(localState, { + data: { + entries: { + test: { + ...tmpFile, + tempFile: true, + changed: true, + }, + }, + treeList: [tmpFile], + }, + projectId: 'gitlab-ce', + branchId: 'master', + }); + + expect(localState.trees['gitlab-ce/master'].tree.length).toEqual(1); + expect(localState.entries.test.tempFile).toEqual(true); + }); + + it('marks entry as replacing previous entry if the old one has been deleted', () => { + const tmpFile = file('test'); + localState.entries.test = { + ...tmpFile, + deleted: true, + }; + mutations.CREATE_TMP_ENTRY(localState, { + data: { + entries: { + test: { + ...tmpFile, + tempFile: true, + changed: true, + }, + }, + treeList: [tmpFile], + }, + projectId: 'gitlab-ce', + branchId: 'master', + }); + + expect(localState.trees['gitlab-ce/master'].tree.length).toEqual(1); + expect(localState.entries.test.replaces).toEqual(true); + }); + }); + describe('UPDATE_TEMP_FLAG', () => { beforeEach(() => { localState.entries.test = { @@ -252,6 +318,7 @@ describe('Multi-file store mutations', () => { permalink: `${gl.TEST_HOST}/testing-123`, commitsPath: `${gl.TEST_HOST}/testing-123`, blamePath: `${gl.TEST_HOST}/testing-123`, + replaces: true, }; localState.entries.test = f; localState.changedFiles.push(f); @@ -262,6 +329,7 @@ describe('Multi-file store mutations', () => { expect(f.permalink).toBe(`${gl.TEST_HOST}/test`); expect(f.commitsPath).toBe(`${gl.TEST_HOST}/test`); expect(f.blamePath).toBe(`${gl.TEST_HOST}/test`); + expect(f.replaces).toBe(false); }); }); @@ -309,7 +377,7 @@ describe('Multi-file store mutations', () => { ...localState.entries.oldPath, id: 'newPath', name: 'newPath', - key: 'newPath-blob-name', + key: 'newPath-blob-oldPath', path: 'newPath', tempFile: true, prevPath: 'oldPath', @@ -318,6 +386,7 @@ describe('Multi-file store mutations', () => { url: `${gl.TEST_HOST}/newPath`, moved: jasmine.anything(), movedPath: jasmine.anything(), + opened: false, }); }); @@ -349,13 +418,5 @@ describe('Multi-file store mutations', () => { expect(localState.entries.parentPath.tree.length).toBe(1); }); - - it('adds to openFiles if previously opened', () => { - localState.entries.oldPath.opened = true; - - mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); - - expect(localState.openFiles).toEqual([localState.entries.newPath]); - }); }); }); diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js index debe1c4acee..0fc9519a6bf 100644 --- a/spec/javascripts/ide/stores/utils_spec.js +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -92,6 +92,16 @@ describe('Multi-file store utils', () => { path: 'deletedFile', deleted: true, }, + { + ...file('renamedFile'), + path: 'renamedFile', + prevPath: 'prevPath', + }, + { + ...file('replacingFile'), + path: 'replacingFile', + replaces: true, + }, ], currentBranchId: 'master', }; @@ -131,8 +141,24 @@ describe('Multi-file store utils', () => { last_commit_id: undefined, previous_path: undefined, }, + { + action: commitActionTypes.move, + file_path: 'renamedFile', + content: null, + encoding: 'text', + last_commit_id: undefined, + previous_path: 'prevPath', + }, + { + action: commitActionTypes.update, + file_path: 'replacingFile', + content: undefined, + encoding: 'text', + last_commit_id: undefined, + previous_path: undefined, + }, ], - start_branch: undefined, + start_sha: undefined, }); }); @@ -187,7 +213,7 @@ describe('Multi-file store utils', () => { previous_path: undefined, }, ], - start_branch: undefined, + start_sha: undefined, }); }); }); @@ -235,6 +261,41 @@ describe('Multi-file store utils', () => { }, ]); }); + + it('filters out folders from the list', () => { + const files = [ + { + path: 'a', + type: 'blob', + deleted: true, + }, + { + path: 'c', + type: 'tree', + deleted: true, + }, + { + path: 'c/d', + type: 'blob', + deleted: true, + }, + ]; + + const flattendFiles = utils.getCommitFiles(files); + + expect(flattendFiles).toEqual([ + { + path: 'a', + type: 'blob', + deleted: true, + }, + { + path: 'c/d', + type: 'blob', + deleted: true, + }, + ]); + }); }); describe('mergeTrees', () => { diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js index 25543053eba..4d57bfb1b33 100644 --- a/spec/javascripts/issuable_spec.js +++ b/spec/javascripts/issuable_spec.js @@ -2,14 +2,14 @@ import $ from 'jquery'; import MockAdaptor from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import IssuableIndex from '~/issuable_index'; +import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar'; describe('Issuable', () => { - let Issuable; describe('initBulkUpdate', () => { it('should not set bulkUpdateSidebar', () => { - Issuable = new IssuableIndex('issue_'); + new IssuableIndex('issue_'); // eslint-disable-line no-new - expect(Issuable.bulkUpdateSidebar).not.toBeDefined(); + expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeNull(); }); it('should set bulkUpdateSidebar', () => { @@ -17,9 +17,9 @@ describe('Issuable', () => { element.classList.add('issues-bulk-update'); document.body.appendChild(element); - Issuable = new IssuableIndex('issue_'); + new IssuableIndex('issue_'); // eslint-disable-line no-new - expect(Issuable.bulkUpdateSidebar).toBeDefined(); + expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeDefined(); }); }); @@ -36,7 +36,7 @@ describe('Issuable', () => { input.setAttribute('id', 'issuable_email'); document.body.appendChild(input); - Issuable = new IssuableIndex('issue_'); + new IssuableIndex('issue_'); // eslint-disable-line no-new mock = new MockAdaptor(axios); diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js index b0f4ab2b12d..a111333ac80 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -1,81 +1,98 @@ import Vue from 'vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import formComponent from '~/issue_show/components/form.vue'; import eventHub from '~/issue_show/event_hub'; describe('Inline edit form component', () => { let vm; - let autosave; - let autosaveObj; - - beforeEach(done => { - autosaveObj = { reset: jasmine.createSpy() }; - - autosave = spyOnDependency(formComponent, 'Autosave').and.returnValue(autosaveObj); + const defaultProps = { + canDestroy: true, + formState: { + title: 'b', + description: 'a', + lockedWarningVisible: false, + }, + issuableType: 'issue', + markdownPreviewPath: '/', + markdownDocsPath: '/', + projectPath: '/', + projectNamespace: '/', + }; + + afterEach(() => { + vm.$destroy(); + }); + const createComponent = props => { const Component = Vue.extend(formComponent); - vm = new Component({ - propsData: { - canDestroy: true, - formState: { - title: 'b', - description: 'a', - lockedWarningVisible: false, - }, - issuableType: 'issue', - markdownPreviewPath: '/', - markdownDocsPath: '/', - projectPath: '/', - projectNamespace: '/', - }, - }).$mount(); - - Vue.nextTick(done); - }); + vm = mountComponent(Component, { + ...defaultProps, + ...props, + }); + }; it('does not render template selector if no templates exist', () => { + createComponent(); + expect(vm.$el.querySelector('.js-issuable-selector-wrap')).toBeNull(); }); - it('renders template selector when templates exists', done => { - vm.issuableTemplates = ['test']; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull(); + it('renders template selector when templates exists', () => { + createComponent({ issuableTemplates: ['test'] }); - done(); - }); + expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull(); }); it('hides locked warning by default', () => { + createComponent(); + expect(vm.$el.querySelector('.alert')).toBeNull(); }); - it('shows locked warning if formState is different', done => { - vm.formState.lockedWarningVisible = true; + it('shows locked warning if formState is different', () => { + createComponent({ formState: { ...defaultProps.formState, lockedWarningVisible: true } }); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.alert')).not.toBeNull(); + expect(vm.$el.querySelector('.alert')).not.toBeNull(); + }); - done(); + it('hides locked warning when currently saving', () => { + createComponent({ + formState: { ...defaultProps.formState, updateLoading: true, lockedWarningVisible: true }, }); - }); - it('initialized Autosave on mount', () => { - expect(autosave).toHaveBeenCalledTimes(2); + expect(vm.$el.querySelector('.alert')).toBeNull(); }); - it('calls reset on autosave when eventHub emits appropriate events', () => { - eventHub.$emit('close.form'); + describe('autosave', () => { + let autosaveObj; + let autosave; + + beforeEach(() => { + autosaveObj = { reset: jasmine.createSpy() }; + autosave = spyOnDependency(formComponent, 'Autosave').and.returnValue(autosaveObj); + }); + + it('initialized Autosave on mount', () => { + createComponent(); - expect(autosaveObj.reset).toHaveBeenCalledTimes(2); + expect(autosave).toHaveBeenCalledTimes(2); + }); + + it('calls reset on autosave when eventHub emits appropriate events', () => { + createComponent(); + + eventHub.$emit('close.form'); - eventHub.$emit('delete.issuable'); + expect(autosaveObj.reset).toHaveBeenCalledTimes(2); - expect(autosaveObj.reset).toHaveBeenCalledTimes(4); + eventHub.$emit('delete.issuable'); - eventHub.$emit('update.issuable'); + expect(autosaveObj.reset).toHaveBeenCalledTimes(4); - expect(autosaveObj.reset).toHaveBeenCalledTimes(6); + eventHub.$emit('update.issuable'); + + expect(autosaveObj.reset).toHaveBeenCalledTimes(6); + }); }); }); diff --git a/spec/javascripts/jobs/components/empty_state_spec.js b/spec/javascripts/jobs/components/empty_state_spec.js new file mode 100644 index 00000000000..c6eac4e27b3 --- /dev/null +++ b/spec/javascripts/jobs/components/empty_state_spec.js @@ -0,0 +1,141 @@ +import Vue from 'vue'; +import component from '~/jobs/components/empty_state.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Empty State', () => { + const Component = Vue.extend(component); + let vm; + + const props = { + illustrationPath: 'illustrations/pending_job_empty.svg', + illustrationSizeClass: 'svg-430', + title: 'This job has not started yet', + playable: false, + variablesSettingsUrl: '', + }; + + const content = 'This job is in pending state and is waiting to be picked by a runner'; + + afterEach(() => { + vm.$destroy(); + }); + + describe('renders image and title', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...props, + content, + }); + }); + + it('renders img with provided path and size', () => { + expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(props.illustrationPath); + expect(vm.$el.querySelector('.svg-content').classList).toContain(props.illustrationSizeClass); + }); + + it('renders provided title', () => { + expect(vm.$el.querySelector('.js-job-empty-state-title').textContent.trim()).toEqual( + props.title, + ); + }); + }); + + describe('with content', () => { + it('renders content', () => { + vm = mountComponent(Component, { + ...props, + content, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-content').textContent.trim()).toEqual( + content, + ); + }); + }); + + describe('without content', () => { + it('does not render content', () => { + vm = mountComponent(Component, { + ...props, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-content')).toBeNull(); + }); + }); + + describe('with action', () => { + it('renders action', () => { + vm = mountComponent(Component, { + ...props, + content, + action: { + path: 'runner', + button_title: 'Check runner', + method: 'post', + }, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-action').getAttribute('href')).toEqual( + 'runner', + ); + }); + }); + + describe('without action', () => { + it('does not render action', () => { + vm = mountComponent(Component, { + ...props, + content, + action: null, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull(); + }); + }); + + describe('without playbale action', () => { + it('does not render manual variables form', () => { + vm = mountComponent(Component, { + ...props, + content, + }); + + expect(vm.$el.querySelector('.js-manual-vars-form')).toBeNull(); + }); + }); + + describe('with playbale action and not scheduled job', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...props, + content, + playable: true, + scheduled: false, + action: { + path: 'runner', + button_title: 'Check runner', + method: 'post', + }, + }); + }); + + it('renders manual variables form', () => { + expect(vm.$el.querySelector('.js-manual-vars-form')).not.toBeNull(); + }); + + it('does not render the empty state action', () => { + expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull(); + }); + }); + + describe('with playbale action and scheduled job', () => { + it('does not render manual variables form', () => { + vm = mountComponent(Component, { + ...props, + content, + }); + + expect(vm.$el.querySelector('.js-manual-vars-form')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index f28d2c2a882..d3c1cf831bb 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -3,7 +3,9 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import jobApp from '~/jobs/components/job_app.vue'; import createStore from '~/jobs/store'; +import * as types from '~/jobs/store/mutation_types'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { waitForMutation } from 'spec/helpers/vue_test_utils_helper'; import { resetStore } from '../store/helpers'; import job from '../mock_data'; @@ -19,12 +21,24 @@ describe('Job App ', () => { runnerHelpUrl: 'help/runner', deploymentHelpUrl: 'help/deployment', runnerSettingsUrl: 'settings/ci-cd/runners', + variablesSettingsUrl: 'settings/ci-cd/variables', terminalPath: 'jobs/123/terminal', pagePath: `${gl.TEST_HOST}jobs/123`, + projectPath: 'user-name/project-name', logState: 'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D', }; + const waitForJobReceived = () => waitForMutation(store, types.RECEIVE_JOB_SUCCESS); + const setupAndMount = ({ jobData = {}, traceData = {} } = {}) => { + mock.onGet(props.endpoint).replyOnce(200, { ...job, ...jobData }); + mock.onGet(`${props.pagePath}/trace.json`).reply(200, traceData); + + vm = mountComponentWithStore(Component, { props, store }); + + return waitForJobReceived(); + }; + beforeEach(() => { mock = new MockAdapter(axios); store = createStore(); @@ -38,103 +52,81 @@ describe('Job App ', () => { describe('while loading', () => { beforeEach(() => { - mock.onGet(props.endpoint).reply(200, job, {}); - mock.onGet(`${props.pagePath}/trace.json`).reply(200, {}); - vm = mountComponentWithStore(Component, { props, store }); + setupAndMount(); }); - it('renders loading icon', done => { + it('renders loading icon', () => { expect(vm.$el.querySelector('.js-job-loading')).not.toBeNull(); expect(vm.$el.querySelector('.js-job-sidebar')).toBeNull(); expect(vm.$el.querySelector('.js-job-content')).toBeNull(); - - setTimeout(() => { - done(); - }, 0); }); }); describe('with successful request', () => { - beforeEach(() => { - mock.onGet(`${props.pagePath}/trace.json`).replyOnce(200, {}); - }); - describe('Header section', () => { describe('job callout message', () => { it('should not render the reason when reason is absent', done => { - mock.onGet(props.endpoint).replyOnce(200, job); - vm = mountComponentWithStore(Component, { props, store }); - - setTimeout(() => { - expect(vm.shouldRenderCalloutMessage).toBe(false); - - done(); - }, 0); + setupAndMount() + .then(() => { + expect(vm.shouldRenderCalloutMessage).toBe(false); + }) + .then(done) + .catch(done.fail); }); it('should render the reason when reason is present', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { - callout_message: 'There is an unknown failure, please try again', - }), - ); - - vm = mountComponentWithStore(Component, { props, store }); - setTimeout(() => { - expect(vm.shouldRenderCalloutMessage).toBe(true); - done(); - }, 0); + setupAndMount({ + jobData: { + callout_message: 'There is an unkown failure, please try again', + }, + }) + .then(() => { + expect(vm.shouldRenderCalloutMessage).toBe(true); + }) + .then(done) + .catch(done.fail); }); }); describe('triggered job', () => { - beforeEach(() => { + beforeEach(done => { const aYearAgo = new Date(); aYearAgo.setFullYear(aYearAgo.getFullYear() - 1); - mock - .onGet(props.endpoint) - .replyOnce(200, Object.assign({}, job, { started: aYearAgo.toISOString() })); - vm = mountComponentWithStore(Component, { props, store }); + setupAndMount({ jobData: { started: aYearAgo.toISOString() } }) + .then(done) + .catch(done.fail); }); - it('should render provided job information', done => { - setTimeout(() => { - expect( - vm.$el - .querySelector('.header-main-content') - .textContent.replace(/\s+/g, ' ') - .trim(), - ).toContain('passed Job #4757 triggered 1 year ago by Root'); - done(); - }, 0); + it('should render provided job information', () => { + expect( + vm.$el + .querySelector('.header-main-content') + .textContent.replace(/\s+/g, ' ') + .trim(), + ).toContain('passed Job #4757 triggered 1 year ago by Root'); }); - it('should render new issue link', done => { - setTimeout(() => { - expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual( - job.new_issue_path, - ); - done(); - }, 0); + it('should render new issue link', () => { + expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual( + job.new_issue_path, + ); }); }); describe('created job', () => { it('should render created key', done => { - mock.onGet(props.endpoint).replyOnce(200, job); - vm = mountComponentWithStore(Component, { props, store }); - - setTimeout(() => { - expect( - vm.$el - .querySelector('.header-main-content') - .textContent.replace(/\s+/g, ' ') - .trim(), - ).toContain('passed Job #4757 created 3 weeks ago by Root'); - done(); - }, 0); + setupAndMount() + .then(() => { + expect( + vm.$el + .querySelector('.header-main-content') + .textContent.replace(/\s+/g, ' ') + .trim(), + ).toContain('passed Job #4757 created 3 weeks ago by Root'); + }) + .then(done) + .catch(done.fail); }); }); }); @@ -142,9 +134,8 @@ describe('Job App ', () => { describe('stuck block', () => { describe('without active runners availabl', () => { it('renders stuck block when there are no runners', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { + setupAndMount({ + jobData: { status: { group: 'pending', icon: 'status_pending', @@ -158,23 +149,23 @@ describe('Job App ', () => { online: false, }, tags: [], - }), - ); - vm = mountComponentWithStore(Component, { props, store }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); - expect(vm.$el.querySelector('.js-job-stuck .js-stuck-no-active-runner')).not.toBeNull(); - done(); - }, 0); + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); + expect( + vm.$el.querySelector('.js-job-stuck .js-stuck-no-active-runner'), + ).not.toBeNull(); + }) + .then(done) + .catch(done.fail); }); }); describe('when available runners can not run specified tag', () => { it('renders tags in stuck block when there are no runners', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { + setupAndMount({ + jobData: { status: { group: 'pending', icon: 'status_pending', @@ -187,27 +178,21 @@ describe('Job App ', () => { available: false, online: false, }, - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull(); - done(); - }, 0); + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull(); + }) + .then(done) + .catch(done.fail); }); }); describe('when runners are offline and build has tags', () => { it('renders message about job being stuck because of no runners with the specified tags', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { + setupAndMount({ + jobData: { status: { group: 'pending', icon: 'status_pending', @@ -220,48 +205,35 @@ describe('Job App ', () => { available: true, online: true, }, - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull(); - done(); - }, 0); + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull(); + }) + .then(done) + .catch(done.fail); }); }); it('does not renders stuck block when there are no runners', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { + setupAndMount({ + jobData: { runners: { available: true }, - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-stuck')).toBeNull(); - - done(); - }, 0); + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-job-stuck')).toBeNull(); + }) + .then(done) + .catch(done.fail); }); }); describe('unmet prerequisites block', () => { it('renders unmet prerequisites block when there is an unmet prerequisites failure', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { + setupAndMount({ + jobData: { status: { group: 'failed', icon: 'status_failed', @@ -281,104 +253,81 @@ describe('Job App ', () => { available: true, }, tags: [], - }), - ); - vm = mountComponentWithStore(Component, { props, store }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-failed')).not.toBeNull(); - done(); - }, 0); + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-job-failed')).not.toBeNull(); + }) + .then(done) + .catch(done.fail); }); }); describe('environments block', () => { it('renders environment block when job has environment', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { + setupAndMount({ + jobData: { deployment_status: { environment: { environment_path: '/path', name: 'foo', }, }, - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-environment')).not.toBeNull(); - - done(); - }, 0); + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-job-environment')).not.toBeNull(); + }) + .then(done) + .catch(done.fail); }); it('does not render environment block when job has environment', done => { - mock.onGet(props.endpoint).replyOnce(200, job); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-environment')).toBeNull(); - done(); - }, 0); + setupAndMount() + .then(() => { + expect(vm.$el.querySelector('.js-job-environment')).toBeNull(); + }) + .then(done) + .catch(done.fail); }); }); describe('erased block', () => { it('renders erased block when `erased` is true', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { + setupAndMount({ + jobData: { erased_by: { username: 'root', web_url: 'gitlab.com/root', }, erased_at: '2016-11-07T11:11:16.525Z', - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-erased-block')).not.toBeNull(); - - done(); - }, 0); + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-job-erased-block')).not.toBeNull(); + }) + .then(done) + .catch(done.fail); }); it('does not render erased block when `erased` is false', done => { - mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { erased_at: null })); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-erased-block')).toBeNull(); - - done(); - }, 0); + setupAndMount({ + jobData: { + erased_at: null, + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-job-erased-block')).toBeNull(); + }) + .then(done) + .catch(done.fail); }); }); describe('empty states block', () => { it('renders empty state when job does not have trace and is not running', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { + setupAndMount({ + jobData: { has_trace: false, status: { group: 'pending', @@ -398,25 +347,18 @@ describe('Job App ', () => { path: '/path', }, }, - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull(); - - done(); - }, 0); + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull(); + }) + .then(done) + .catch(done.fail); }); it('does not render empty state when job does not have trace but it is running', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { + setupAndMount({ + jobData: { has_trace: false, status: { group: 'running', @@ -425,34 +367,23 @@ describe('Job App ', () => { text: 'running', details_path: 'path', }, - }), - ); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull(); - - done(); - }, 0); + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull(); + }) + .then(done) + .catch(done.fail); }); it('does not render empty state when job has trace but it is not running', done => { - mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { has_trace: true })); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull(); - - done(); - }, 0); + setupAndMount({ jobData: { has_trace: true } }) + .then(() => { + expect(vm.$el.querySelector('.js-job-empty-state')).toBeNull(); + }) + .then(done) + .catch(done.fail); + done(); }); it('displays remaining time for a delayed job', done => { @@ -460,120 +391,114 @@ describe('Job App ', () => { spyOn(Date, 'now').and.callFake( () => new Date(delayedJobFixture.scheduled_at).getTime() - oneHourInMilliseconds, ); - mock.onGet(props.endpoint).replyOnce(200, { ...delayedJobFixture }); + setupAndMount({ jobData: delayedJobFixture }) + .then(() => { + expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull(); - vm = mountComponentWithStore(Component, { - props, - store, - }); - - store.subscribeAction(action => { - if (action.type !== 'receiveJobSuccess') { - return; - } + const title = vm.$el.querySelector('.js-job-empty-state-title'); - Vue.nextTick() - .then(() => { - expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull(); - - const title = vm.$el.querySelector('.js-job-empty-state-title'); + expect(title).toContainText('01:00:00'); + }) + .then(done) + .catch(done.fail); + }); + }); - expect(title).toContainText('01:00:00'); - done(); - }) - .catch(done.fail); - }); + describe('sidebar', () => { + it('has no blank blocks', done => { + setupAndMount({ + jobData: { + duration: null, + finished_at: null, + erased_at: null, + queued: null, + runner: null, + coverage: null, + tags: [], + cancel_path: null, + }, + }) + .then(() => { + vm.$el.querySelectorAll('.blocks-container > *').forEach(block => { + expect(block.textContent.trim()).not.toBe(''); + }); + }) + .then(done) + .catch(done.fail); }); }); }); describe('archived job', () => { - beforeEach(() => { - mock.onGet(props.endpoint).reply(200, Object.assign({}, job, { archived: true }), {}); - vm = mountComponentWithStore(Component, { - props, - store, - }); + beforeEach(done => { + setupAndMount({ jobData: { archived: true } }) + .then(done) + .catch(done.fail); }); - it('renders warning about job being archived', done => { - setTimeout(() => { - expect(vm.$el.querySelector('.js-archived-job ')).not.toBeNull(); - done(); - }, 0); + it('renders warning about job being archived', () => { + expect(vm.$el.querySelector('.js-archived-job ')).not.toBeNull(); }); }); describe('non-archived job', () => { - beforeEach(() => { - mock.onGet(props.endpoint).reply(200, job, {}); - vm = mountComponentWithStore(Component, { - props, - store, - }); + beforeEach(done => { + setupAndMount() + .then(done) + .catch(done.fail); }); - it('does not warning about job being archived', done => { - setTimeout(() => { - expect(vm.$el.querySelector('.js-archived-job ')).toBeNull(); - done(); - }, 0); + it('does not warning about job being archived', () => { + expect(vm.$el.querySelector('.js-archived-job ')).toBeNull(); }); }); describe('trace output', () => { - beforeEach(() => { - mock.onGet(props.endpoint).reply(200, job, {}); - }); - describe('with append flag', () => { it('appends the log content to the existing one', done => { - mock.onGet(`${props.pagePath}/trace.json`).reply(200, { - html: '<span>More<span>', - status: 'running', - state: 'newstate', - append: true, - complete: true, - }); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - vm.$store.state.trace = 'Update'; - - setTimeout(() => { - expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Update'); - - done(); - }, 0); + setupAndMount({ + traceData: { + html: '<span>More<span>', + status: 'running', + state: 'newstate', + append: true, + complete: true, + }, + }) + .then(() => { + vm.$store.state.trace = 'Update'; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Update'); + }) + .then(done) + .catch(done.fail); }); }); describe('without append flag', () => { it('replaces the trace', done => { - mock.onGet(`${props.pagePath}/trace.json`).reply(200, { - html: '<span>Different<span>', - status: 'running', - append: false, - complete: true, - }); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - vm.$store.state.trace = 'Update'; - - setTimeout(() => { - expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain( - 'Update', - ); + setupAndMount({ + traceData: { + html: '<span>Different<span>', + status: 'running', + append: false, + complete: true, + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain( + 'Update', + ); - expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Different'); - done(); - }, 0); + expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain( + 'Different', + ); + }) + .then(done) + .catch(done.fail); }); }); @@ -589,83 +514,76 @@ describe('Job App ', () => { complete: true, }); - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-truncated-info').textContent.trim()).toContain( - '50 bytes', - ); - done(); - }, 0); + setupAndMount({ + traceData: { + html: '<span>Update</span>', + status: 'success', + append: false, + size: 50, + total: 100, + complete: true, + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-truncated-info').textContent.trim()).toContain( + '50 bytes', + ); + }) + .then(done) + .catch(done.fail); }); }); describe('when size is equal than total', () => { it('does not show the truncated information', done => { - mock.onGet(`${props.pagePath}/trace.json`).reply(200, { - html: '<span>Update</span>', - status: 'success', - append: false, - size: 100, - total: 100, - complete: true, - }); - - vm = mountComponentWithStore(Component, { - props, - store, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-truncated-info').textContent.trim()).not.toContain( - '50 bytes', - ); - done(); - }, 0); + setupAndMount({ + traceData: { + html: '<span>Update</span>', + status: 'success', + append: false, + size: 100, + total: 100, + complete: true, + }, + }) + .then(() => { + expect(vm.$el.querySelector('.js-truncated-info').textContent.trim()).not.toContain( + '50 bytes', + ); + }) + .then(done) + .catch(done.fail); }); }); }); describe('trace controls', () => { - beforeEach(() => { - mock.onGet(`${props.pagePath}/trace.json`).reply(200, { - html: '<span>Update</span>', - status: 'success', - append: false, - size: 50, - total: 100, - complete: true, - }); - - vm = mountComponentWithStore(Component, { - props, - store, - }); + beforeEach(done => { + setupAndMount({ + traceData: { + html: '<span>Update</span>', + status: 'success', + append: false, + size: 50, + total: 100, + complete: true, + }, + }) + .then(done) + .catch(done.fail); }); - it('should render scroll buttons', done => { - setTimeout(() => { - expect(vm.$el.querySelector('.js-scroll-top')).not.toBeNull(); - expect(vm.$el.querySelector('.js-scroll-bottom')).not.toBeNull(); - done(); - }, 0); + it('should render scroll buttons', () => { + expect(vm.$el.querySelector('.js-scroll-top')).not.toBeNull(); + expect(vm.$el.querySelector('.js-scroll-bottom')).not.toBeNull(); }); - it('should render link to raw ouput', done => { - setTimeout(() => { - expect(vm.$el.querySelector('.js-raw-link-controller')).not.toBeNull(); - done(); - }, 0); + it('should render link to raw ouput', () => { + expect(vm.$el.querySelector('.js-raw-link-controller')).not.toBeNull(); }); - it('should render link to erase job', done => { - setTimeout(() => { - expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull(); - done(); - }, 0); + it('should render link to erase job', () => { + expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull(); }); }); }); diff --git a/spec/javascripts/jobs/components/job_log_spec.js b/spec/javascripts/jobs/components/job_log_spec.js index dc0f77ceb80..24bb6b9a48b 100644 --- a/spec/javascripts/jobs/components/job_log_spec.js +++ b/spec/javascripts/jobs/components/job_log_spec.js @@ -3,6 +3,7 @@ import component from '~/jobs/components/job_log.vue'; import createStore from '~/jobs/store'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../store/helpers'; +import { logWithCollapsibleSections } from '../mock_data'; describe('Job Log', () => { const Component = Vue.extend(component); @@ -10,7 +11,7 @@ describe('Job Log', () => { let vm; const trace = - 'Running with gitlab-runner 11.1.0 (081978aa)<br> on docker-auto-scale-com d5ae8d25<br>Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29 ...<br>'; + '<span>Running with gitlab-runner 12.1.0 (de7731dd)<br/></span><span> on docker-auto-scale-com d5ae8d25<br/></span><div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="1565502765" data-section="prepare-executor" role="button"></div><span class="section js-section-header section-header js-s-prepare-executor">Using Docker executor with image ruby:2.6 ...<br/></span>'; beforeEach(() => { store = createStore(); @@ -31,7 +32,7 @@ describe('Job Log', () => { }); expect(vm.$el.querySelector('code').textContent).toContain( - 'Running with gitlab-runner 11.1.0 (081978aa)', + 'Running with gitlab-runner 12.1.0 (de7731dd)', ); }); @@ -62,4 +63,60 @@ describe('Job Log', () => { expect(vm.$el.querySelector('.js-log-animation')).toBeNull(); }); }); + + describe('Collapsible sections', () => { + beforeEach(() => { + vm = mountComponentWithStore(Component, { + props: { + trace: logWithCollapsibleSections.html, + isComplete: true, + }, + store, + }); + }); + + it('renders open arrow', () => { + expect(vm.$el.querySelector('.fa-caret-down')).not.toBeNull(); + }); + + it('toggles hidden class to the sibilings rows when arrow is clicked', done => { + vm.$nextTick() + .then(() => { + const { section } = vm.$el.querySelector('.js-section-start').dataset; + vm.$el.querySelector('.js-section-start').click(); + + vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => { + expect(el.classList.contains('hidden')).toEqual(true); + }); + + vm.$el.querySelector('.js-section-start').click(); + + vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => { + expect(el.classList.contains('hidden')).toEqual(false); + }); + }) + .then(done) + .catch(done.fail); + }); + + it('toggles hidden class to the sibilings rows when header section is clicked', done => { + vm.$nextTick() + .then(() => { + const { section } = vm.$el.querySelector('.js-section-header').dataset; + vm.$el.querySelector('.js-section-header').click(); + + vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => { + expect(el.classList.contains('hidden')).toEqual(true); + }); + + vm.$el.querySelector('.js-section-header').click(); + + vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => { + expect(el.classList.contains('hidden')).toEqual(false); + }); + }) + .then(done) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/jobs/components/manual_variables_form_spec.js b/spec/javascripts/jobs/components/manual_variables_form_spec.js new file mode 100644 index 00000000000..093aa905185 --- /dev/null +++ b/spec/javascripts/jobs/components/manual_variables_form_spec.js @@ -0,0 +1,88 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import Form from '~/jobs/components/manual_variables_form.vue'; + +describe('Manual Variables Form', () => { + let wrapper; + const requiredProps = { + action: { + path: '/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + variablesSettingsUrl: '/settings', + }; + + const factory = (props = {}) => { + wrapper = shallowMount(Form, { + propsData: props, + }); + }; + + beforeEach(() => { + factory(requiredProps); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders empty form with correct placeholders', () => { + expect(wrapper.find({ ref: 'inputKey' }).attributes('placeholder')).toBe('Input variable key'); + expect(wrapper.find({ ref: 'inputSecretValue' }).attributes('placeholder')).toBe( + 'Input variable value', + ); + }); + + it('renders help text with provided link', () => { + expect(wrapper.find('p').text()).toBe( + 'Specify variable values to be used in this run. The values specified in CI/CD settings will be used as default', + ); + + expect(wrapper.find('a').attributes('href')).toBe(requiredProps.variablesSettingsUrl); + }); + + describe('when adding a new variable', () => { + it('creates a new variable when user types a new key and resets the form', done => { + wrapper.vm + .$nextTick() + .then(() => wrapper.find({ ref: 'inputKey' }).setValue('new key')) + .then(() => { + expect(wrapper.vm.variables.length).toBe(1); + expect(wrapper.vm.variables[0].key).toBe('new key'); + expect(wrapper.find({ ref: 'inputKey' }).attributes('value')).toBe(undefined); + }) + .then(done) + .catch(done.fail); + }); + + it('creates a new variable when user types a new value and resets the form', done => { + wrapper.vm + .$nextTick() + .then(() => wrapper.find({ ref: 'inputSecretValue' }).setValue('new value')) + .then(() => { + expect(wrapper.vm.variables.length).toBe(1); + expect(wrapper.vm.variables[0].secret_value).toBe('new value'); + expect(wrapper.find({ ref: 'inputSecretValue' }).attributes('value')).toBe(undefined); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when deleting a variable', () => { + it('removes the variable row', () => { + wrapper.vm.variables = [ + { + key: 'new key', + secret_value: 'value', + id: '1', + }, + ]; + + wrapper.find(GlButton).vm.$emit('click'); + + expect(wrapper.vm.variables.length).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/jobs/components/stages_dropdown_spec.js b/spec/javascripts/jobs/components/stages_dropdown_spec.js index e98639bf21e..86b7a8d7848 100644 --- a/spec/javascripts/jobs/components/stages_dropdown_spec.js +++ b/spec/javascripts/jobs/components/stages_dropdown_spec.js @@ -9,7 +9,6 @@ describe('Stages Dropdown', () => { const mockPipelineData = { id: 28029444, - iid: 123, details: { status: { details_path: '/gitlab-org/gitlab-ce/pipelines/28029444', @@ -78,8 +77,8 @@ describe('Stages Dropdown', () => { expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy'); }); - it(`renders the pipeline info text like "Pipeline #123 (#12) for source_branch"`, () => { - const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for ${pipeline.ref.name}`; + it(`renders the pipeline info text like "Pipeline #123 for source_branch"`, () => { + const expected = `Pipeline #${pipeline.id} for ${pipeline.ref.name}`; const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText); expect(actual).toBe(expected); @@ -101,10 +100,8 @@ describe('Stages Dropdown', () => { }); }); - it(`renders the pipeline info text like "Pipeline #123 (#12) for !456 with source_branch into target_branch"`, () => { - const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for !${ - pipeline.merge_request.iid - } with ${pipeline.merge_request.source_branch} into ${pipeline.merge_request.target_branch}`; + it(`renders the pipeline info text like "Pipeline #123 for !456 with source_branch into target_branch"`, () => { + const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch} into ${pipeline.merge_request.target_branch}`; const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText); expect(actual).toBe(expected); @@ -144,10 +141,8 @@ describe('Stages Dropdown', () => { }); }); - it(`renders the pipeline info like "Pipeline #123 (#12) for !456 with source_branch"`, () => { - const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for !${ - pipeline.merge_request.iid - } with ${pipeline.merge_request.source_branch}`; + it(`renders the pipeline info like "Pipeline #123 for !456 with source_branch"`, () => { + const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch}`; const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText); expect(actual).toBe(expected); diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index 88b0bb206ee..c5022d3e93d 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -960,7 +960,6 @@ export default { }, pipeline: { id: 140, - iid: 13, user: { name: 'Root', username: 'root', @@ -1190,3 +1189,18 @@ export const jobsInStage = { path: '/gitlab-org/gitlab-shell/pipelines/27#build', dropdown_path: '/gitlab-org/gitlab-shell/pipelines/27/stage.json?stage=build', }; + +export const logWithCollapsibleSections = { + append: false, + complete: true, + html: + '<div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="1559571405" data-section="after-script" role="button"></div><span class="term-fg-l-green term-bold section js-section-header js-s-after-script">Running after script...</span><span class="section js-section-header js-s-after-script"><br /></span><span class="section s_after-script line"></span><span class="section js-s-after-script"></span><span class="term-fg-l-green term-bold section js-s-after-script">$ date</span><span class="section js-s-after-script"><br /></span><span class="section s_after-script line"></span><span class="section js-s-after-script">Mon Jun 3 14:16:46 UTC 2019<br /></span><span class="section s_after-script line"></span><span class="section js-s-after-script"></span><div class="section-end" data-section="after-script"></div><div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer"data-timestamp="1559571408" data-section="archive-cache" role="button" ></div><span class="term-fg-l-green term-bold section js-section-header js-s-archive-cache">Not uploading cache debian-stretch-ruby-2.6.3-node-10.x-3 due to policy</span><span class="section js-section-header js-s-archive-cache"><br /></span><span class="section s_archive-cache line"></span><span class="section js-s-archive-cache"></span><div class="section-end" data-section="archive-cache"></div><div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="1559571409" data-section="upload-artifacts-on-success" role="button"></div><span class="term-fg-l-green term-bold section js-section-header js-s-upload-artifacts-on-success">Uploading artifacts...</span><span class="section js-section-header js-s-upload-artifacts-on-success"><br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">coverage/: found 5 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">knapsack/: found 4 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">rspec_flaky/: found 4 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">rspec_profiling/: found 1 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success"></span><span class="term-fg-yellow section js-s-upload-artifacts-on-success">WARNING: tmp/capybara/: no matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">Uploading artifacts to coordinator... ok </span><span class="section js-s-upload-artifacts-on-success"> id</span><span class="section js-s-upload-artifacts-on-success">=224162288 responseStatus</span><span class="section js-s-upload-artifacts-on-success">=201 Created token</span><span class="section js-s-upload-artifacts-on-success">=bBmyXJNW<br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success"></span><span class="term-fg-l-green term-bold section js-s-upload-artifacts-on-success">Uploading artifacts...</span><span class="section js-s-upload-artifacts-on-success"><br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">junit_rspec.xml: found 1 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">Uploading artifacts to coordinator... ok </span><span class="section js-s-upload-artifacts-on-success"> id</span><span class="section js-s-upload-artifacts-on-success">=224162288 responseStatus</span><span class="section js-s-upload-artifacts-on-success">=201 Created token</span><span class="section js-s-upload-artifacts-on-success">=bBmyXJNW<br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success"></span><div class="section-end" data-section="upload-artifacts-on-success"></div><span class="term-fg-l-green term-bold">Job succeeded<br /><span class="term-fg-l-green term-bold"></span></span>', + id: 1385, + offset: 0, + size: 78815, + state: + 'eyJvZmZzZXQiOjc4ODE1LCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowLCJzZWN0aW9ucyI6W10sImxpbmVub19pbl9zZWN0aW9uIjoxMX0=', + status: 'success', + total: 78815, + truncated: false, +}; diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 1295d900de7..3a53ecacb88 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -46,15 +46,30 @@ describe('MergeRequestTabs', function() { describe('opensInNewTab', function() { var tabUrl; var windowTarget = '_blank'; + let clickTabParams; beforeEach(function() { loadFixtures('merge_requests/merge_request_with_task_list.html'); tabUrl = $('.commits-tab a').attr('href'); + + clickTabParams = { + metaKey: false, + ctrlKey: false, + which: 1, + stopImmediatePropagation: function() {}, + preventDefault: function() {}, + currentTarget: { + getAttribute: function(attr) { + return attr === 'href' ? tabUrl : null; + }, + }, + }; }); describe('meta click', () => { let metakeyEvent; + beforeEach(function() { metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); }); @@ -67,6 +82,8 @@ describe('MergeRequestTabs', function() { this.class.bindEvents(); $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent); + + expect(window.open).toHaveBeenCalled(); }); it('opens page when commits badge is clicked', function() { @@ -77,6 +94,8 @@ describe('MergeRequestTabs', function() { this.class.bindEvents(); $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent); + + expect(window.open).toHaveBeenCalled(); }); }); @@ -86,12 +105,9 @@ describe('MergeRequestTabs', function() { expect(name).toEqual(windowTarget); }); - this.class.clickTab({ - metaKey: false, - ctrlKey: true, - which: 1, - stopImmediatePropagation: function() {}, - }); + this.class.clickTab({ ...clickTabParams, metaKey: true }); + + expect(window.open).toHaveBeenCalled(); }); it('opens page tab in a new browser tab with Cmd+Click - Mac', function() { @@ -100,12 +116,9 @@ describe('MergeRequestTabs', function() { expect(name).toEqual(windowTarget); }); - this.class.clickTab({ - metaKey: true, - ctrlKey: false, - which: 1, - stopImmediatePropagation: function() {}, - }); + this.class.clickTab({ ...clickTabParams, ctrlKey: true }); + + expect(window.open).toHaveBeenCalled(); }); it('opens page tab in a new browser tab with Middle-click - Mac/PC', function() { @@ -114,12 +127,9 @@ describe('MergeRequestTabs', function() { expect(name).toEqual(windowTarget); }); - this.class.clickTab({ - metaKey: false, - ctrlKey: false, - which: 2, - stopImmediatePropagation: function() {}, - }); + this.class.clickTab({ ...clickTabParams, which: 2 }); + + expect(window.open).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index ac7e0bb12a1..57f99a09002 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -1,21 +1,26 @@ import { shallowMount } from '@vue/test-utils'; +import { createStore } from '~/monitoring/stores'; +import { GlLink } from '@gitlab/ui'; import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper'; import Area from '~/monitoring/components/charts/area.vue'; -import { createStore } from '~/monitoring/stores'; import * as types from '~/monitoring/stores/mutation_types'; +import { TEST_HOST } from 'spec/test_constants'; import MonitoringMock, { deploymentData } from '../mock_data'; describe('Area component', () => { + const mockSha = 'mockSha'; const mockWidgets = 'mockWidgets'; const mockSvgPathContent = 'mockSvgPathContent'; + const projectPath = `${TEST_HOST}/path/to/project`; + const commitUrl = `${projectPath}/commit/${mockSha}`; let mockGraphData; let areaChart; let spriteSpy; + let store; beforeEach(() => { - const store = createStore(); - + store = createStore(); store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data); store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); @@ -26,10 +31,12 @@ describe('Area component', () => { graphData: mockGraphData, containerWidth: 0, deploymentData: store.state.monitoringDashboard.deploymentData, + projectPath, }, slots: { default: mockWidgets, }, + store, }); spriteSpy = spyOnDependency(Area, 'getSvgIconPathContent').and.callFake( @@ -88,11 +95,14 @@ describe('Area component', () => { ); }); - it('renders commit sha in tooltip content', () => { - const mockSha = 'mockSha'; + it('renders clickable commit sha in tooltip content', () => { areaChart.vm.tooltip.sha = mockSha; + areaChart.vm.tooltip.commitUrl = commitUrl; + + const commitLink = areaChart.find(GlLink); - expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipContent', mockSha)).toBe(true); + expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true); + expect(commitLink.attributes('href')).toEqual(commitUrl); }); }); }); diff --git a/spec/javascripts/monitoring/charts/column_spec.js b/spec/javascripts/monitoring/charts/column_spec.js new file mode 100644 index 00000000000..d8ac68b9484 --- /dev/null +++ b/spec/javascripts/monitoring/charts/column_spec.js @@ -0,0 +1,58 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlColumnChart } from '@gitlab/ui/dist/charts'; +import ColumnChart from '~/monitoring/components/charts/column.vue'; + +describe('Column component', () => { + let columnChart; + + beforeEach(() => { + columnChart = shallowMount(ColumnChart, { + propsData: { + graphData: { + queries: [ + { + x_label: 'Time', + y_label: 'Usage', + result: [ + { + metric: {}, + values: [ + [1495700554.925, '8.0390625'], + [1495700614.925, '8.0390625'], + [1495700674.925, '8.0390625'], + ], + }, + ], + }, + ], + }, + containerWidth: 100, + }, + }); + }); + + afterEach(() => { + columnChart.destroy(); + }); + + describe('wrapped components', () => { + describe('GitLab UI column chart', () => { + let glColumnChart; + + beforeEach(() => { + glColumnChart = columnChart.find(GlColumnChart); + }); + + it('is a Vue instance', () => { + expect(glColumnChart.isVueInstance()).toBe(true); + }); + + it('receives data properties needed for proper chart render', () => { + const props = glColumnChart.props(); + + expect(props.data).toBe(columnChart.vm.chartData); + expect(props.option).toBe(columnChart.vm.chartOptions); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/charts/empty_chart_spec.js b/spec/javascripts/monitoring/charts/empty_chart_spec.js new file mode 100644 index 00000000000..bbfca27dc5a --- /dev/null +++ b/spec/javascripts/monitoring/charts/empty_chart_spec.js @@ -0,0 +1,29 @@ +import { shallowMount } from '@vue/test-utils'; +import EmptyChart from '~/monitoring/components/charts/empty_chart.vue'; + +describe('Empty Chart component', () => { + let emptyChart; + const graphTitle = 'Memory Usage'; + + beforeEach(() => { + emptyChart = shallowMount(EmptyChart, { + propsData: { + graphTitle, + }, + }); + }); + + afterEach(() => { + emptyChart.destroy(); + }); + + it('render the chart title', () => { + expect(emptyChart.find({ ref: 'graphTitle' }).text()).toBe(graphTitle); + }); + + describe('Computed props', () => { + it('sets the height for the svg container', () => { + expect(emptyChart.vm.svgContainerStyle.height).toBe('300px'); + }); + }); +}); diff --git a/spec/javascripts/monitoring/charts/single_stat_spec.js b/spec/javascripts/monitoring/charts/single_stat_spec.js index 12b73002f97..127a4a7955a 100644 --- a/spec/javascripts/monitoring/charts/single_stat_spec.js +++ b/spec/javascripts/monitoring/charts/single_stat_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import SingleStatChart from '~/monitoring/components/charts/single_stat.vue'; +import { graphDataPrometheusQuery } from '../mock_data'; describe('Single Stat Chart component', () => { let singleStatChart; @@ -7,9 +8,7 @@ describe('Single Stat Chart component', () => { beforeEach(() => { singleStatChart = shallowMount(SingleStatChart, { propsData: { - title: 'Time to render', - value: 1, - unit: 'sec', + graphData: graphDataPrometheusQuery, }, }); }); @@ -19,9 +18,9 @@ describe('Single Stat Chart component', () => { }); describe('computed', () => { - describe('valueWithUnit', () => { + describe('engineeringNotation', () => { it('should interpolate the value and unit props', () => { - expect(singleStatChart.vm.valueWithUnit).toBe('1sec'); + expect(singleStatChart.vm.engineeringNotation).toBe('91MB'); }); }); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index f1d578648b8..02c3f303912 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -1,17 +1,21 @@ import Vue from 'vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlToast } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import Dashboard from '~/monitoring/components/dashboard.vue'; import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants'; import * as types from '~/monitoring/stores/mutation_types'; import { createStore } from '~/monitoring/stores'; import axios from '~/lib/utils/axios_utils'; -import { +import MonitoringMock, { metricsGroupsAPIResponse, mockApiEndpoint, environmentData, singleGroupResponse, + dashboardGitResponse, } from './mock_data'; +const localVue = createLocalVue(); const propsData = { hasMetrics: false, documentationPath: '/path/to/docs', @@ -20,7 +24,7 @@ const propsData = { tagsPath: '/path/to/tags', projectPath: '/path/to/project', metricsEndpoint: mockApiEndpoint, - deploymentEndpoint: null, + deploymentsEndpoint: null, emptyGettingStartedSvgPath: '/path/to/getting-started.svg', emptyLoadingSvgPath: '/path/to/loading.svg', emptyNoDataSvgPath: '/path/to/no-data.svg', @@ -39,6 +43,7 @@ describe('Dashboard', () => { let mock; let store; let component; + let mockGraphData; beforeEach(() => { setFixtures(` @@ -57,21 +62,41 @@ describe('Dashboard', () => { }); afterEach(() => { - component.$destroy(); + if (component) { + component.$destroy(); + } mock.restore(); }); describe('no metrics are available yet', () => { - it('shows a getting started empty state when no metrics are present', () => { + beforeEach(() => { component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), propsData: { ...propsData }, store, }); + }); + it('shows a getting started empty state when no metrics are present', () => { expect(component.$el.querySelector('.prometheus-graphs')).toBe(null); expect(component.emptyState).toEqual('gettingStarted'); }); + + it('shows the environment selector', () => { + expect(component.$el.querySelector('.js-environments-dropdown')).toBeTruthy(); + }); + }); + + describe('no data found', () => { + it('shows the environment selector dropdown', () => { + component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { ...propsData, showEmptyState: true }, + store, + }); + + expect(component.$el.querySelector('.js-environments-dropdown')).toBeTruthy(); + }); }); describe('requests information to the server', () => { @@ -150,14 +175,24 @@ describe('Dashboard', () => { singleGroupResponse, ); - setTimeout(() => { - const dropdownMenuEnvironments = component.$el.querySelectorAll( - '.js-environments-dropdown .dropdown-item', - ); + Vue.nextTick() + .then(() => { + const dropdownMenuEnvironments = component.$el.querySelectorAll( + '.js-environments-dropdown .dropdown-item', + ); - expect(dropdownMenuEnvironments.length).toEqual(component.environments.length); - done(); - }); + expect(component.environments.length).toEqual(environmentData.length); + expect(dropdownMenuEnvironments.length).toEqual(component.environments.length); + + Array.from(dropdownMenuEnvironments).forEach((value, index) => { + if (environmentData[index].metrics_path) { + expect(value).toHaveAttr('href', environmentData[index].metrics_path); + } + }); + + done(); + }) + .catch(done.fail); }); it('hides the environments dropdown list when there is no environments', done => { @@ -212,7 +247,7 @@ describe('Dashboard', () => { Vue.nextTick() .then(() => { const dropdownItems = component.$el.querySelectorAll( - '.js-environments-dropdown .dropdown-item[active="true"]', + '.js-environments-dropdown .dropdown-item.active', ); expect(dropdownItems.length).toEqual(1); @@ -278,13 +313,9 @@ describe('Dashboard', () => { }); spyOn(component.$store, 'dispatch').and.stub(); - const getTimeDiffSpy = spyOnDependency(Dashboard, 'getTimeDiff'); + const getTimeDiffSpy = spyOnDependency(Dashboard, 'getTimeDiff').and.callThrough(); component.$store.commit( - `monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`, - '/environments', - ); - component.$store.commit( `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData, ); @@ -294,7 +325,7 @@ describe('Dashboard', () => { Vue.nextTick() .then(() => { expect(component.$store.dispatch).toHaveBeenCalled(); - expect(getTimeDiffSpy).toHaveBeenCalledWith(component.selectedTimeWindow); + expect(getTimeDiffSpy).toHaveBeenCalled(); done(); }) @@ -302,7 +333,17 @@ describe('Dashboard', () => { }); it('shows a specific time window selected from the url params', done => { - spyOnDependency(Dashboard, 'getParameterValues').and.returnValue(['thirtyMinutes']); + const start = 1564439536; + const end = 1564441336; + spyOnDependency(Dashboard, 'getTimeDiff').and.returnValue({ + start, + end, + }); + spyOnDependency(Dashboard, 'getParameterValues').and.callFake(param => { + if (param === 'start') return [start]; + if (param === 'end') return [end]; + return []; + }); component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), @@ -337,6 +378,71 @@ describe('Dashboard', () => { }); }); + describe('link to chart', () => { + let wrapper; + const currentDashboard = 'TEST_DASHBOARD'; + localVue.use(GlToast); + const link = () => wrapper.find('.js-chart-link'); + const clipboardText = () => link().element.dataset.clipboardText; + + beforeEach(done => { + mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); + + wrapper = shallowMount(DashboardComponent, { + localVue, + sync: false, + attachToDocument: true, + propsData: { ...propsData, hasMetrics: true, currentDashboard }, + store, + }); + + setTimeout(done); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('adds a copy button to the dropdown', () => { + expect(link().text()).toContain('Generate link to chart'); + }); + + it('contains a link to the dashboard', () => { + expect(clipboardText()).toContain(`dashboard=${currentDashboard}`); + expect(clipboardText()).toContain(`group=`); + expect(clipboardText()).toContain(`title=`); + expect(clipboardText()).toContain(`y_label=`); + }); + + it('undefined parameter is stripped', done => { + wrapper.setProps({ currentDashboard: undefined }); + + wrapper.vm.$nextTick(() => { + expect(clipboardText()).not.toContain(`dashboard=`); + expect(clipboardText()).toContain(`y_label=`); + done(); + }); + }); + + it('null parameter is stripped', done => { + wrapper.setProps({ currentDashboard: null }); + + wrapper.vm.$nextTick(() => { + expect(clipboardText()).not.toContain(`dashboard=`); + expect(clipboardText()).toContain(`y_label=`); + done(); + }); + }); + + it('creates a toast when clicked', () => { + spyOn(wrapper.vm.$toast, 'show').and.stub(); + + link().vm.$emit('click'); + + expect(wrapper.vm.$toast.show).toHaveBeenCalled(); + }); + }); + describe('when the window resizes', () => { beforeEach(() => { mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); @@ -402,4 +508,81 @@ describe('Dashboard', () => { }); }); }); + + describe('Dashboard dropdown', () => { + beforeEach(() => { + mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); + + component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + }, + store, + }); + + component.$store.dispatch('monitoringDashboard/setFeatureFlags', { + prometheusEndpoint: false, + multipleDashboardsEnabled: true, + }); + + component.$store.commit( + `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, + environmentData, + ); + + component.$store.commit( + `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + singleGroupResponse, + ); + + component.$store.commit( + `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, + dashboardGitResponse, + ); + }); + + it('shows the dashboard dropdown', done => { + setTimeout(() => { + const dashboardDropdown = component.$el.querySelector('.js-dashboards-dropdown'); + + expect(dashboardDropdown).not.toEqual(null); + done(); + }); + }); + }); + + describe('when downloading metrics data as CSV', () => { + beforeEach(() => { + component = new DashboardComponent({ + propsData: { + ...propsData, + }, + store, + }); + store.commit( + `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + MonitoringMock.data, + ); + [mockGraphData] = component.$store.state.monitoringDashboard.groups[0].metrics; + }); + + describe('csvText', () => { + it('converts metrics data from json to csv', () => { + const header = `timestamp,${mockGraphData.y_label}`; + const data = mockGraphData.queries[0].result[0].values; + const firstRow = `${data[0][0]},${data[0][1]}`; + + expect(component.csvText(mockGraphData)).toMatch(`^${header}\r\n${firstRow}`); + }); + }); + + describe('downloadCsv', () => { + it('produces a link with a Blob', () => { + expect(component.downloadCsv(mockGraphData)).toContain(`blob:`); + }); + }); + }); }); diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js deleted file mode 100644 index 6b2be83aa8c..00000000000 --- a/spec/javascripts/monitoring/dashboard_state_spec.js +++ /dev/null @@ -1,101 +0,0 @@ -import Vue from 'vue'; -import EmptyState from '~/monitoring/components/empty_state.vue'; -import { statePaths } from './mock_data'; - -function createComponent(props) { - const Component = Vue.extend(EmptyState); - - return new Component({ - propsData: { - ...props, - settingsPath: statePaths.settingsPath, - clustersPath: statePaths.clustersPath, - documentationPath: statePaths.documentationPath, - emptyGettingStartedSvgPath: '/path/to/getting-started.svg', - emptyLoadingSvgPath: '/path/to/loading.svg', - emptyNoDataSvgPath: '/path/to/no-data.svg', - emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', - }, - }).$mount(); -} - -function getTextFromNode(component, selector) { - return component.$el.querySelector(selector).firstChild.nodeValue.trim(); -} - -describe('EmptyState', () => { - describe('Computed props', () => { - it('currentState', () => { - const component = createComponent({ - selectedState: 'gettingStarted', - }); - - expect(component.currentState).toBe(component.states.gettingStarted); - }); - - it('showButtonDescription returns a description with a link for the unableToConnect state', () => { - const component = createComponent({ - selectedState: 'unableToConnect', - }); - - expect(component.showButtonDescription).toEqual(true); - }); - - it('showButtonDescription returns the description without a link for any other state', () => { - const component = createComponent({ - selectedState: 'loading', - }); - - expect(component.showButtonDescription).toEqual(false); - }); - }); - - it('should show the gettingStarted state', () => { - const component = createComponent({ - selectedState: 'gettingStarted', - }); - - expect(component.$el.querySelector('svg')).toBeDefined(); - expect(getTextFromNode(component, '.state-title')).toEqual( - component.states.gettingStarted.title, - ); - - expect(getTextFromNode(component, '.state-description')).toEqual( - component.states.gettingStarted.description, - ); - - expect(getTextFromNode(component, '.btn-success')).toEqual( - component.states.gettingStarted.buttonText, - ); - }); - - it('should show the loading state', () => { - const component = createComponent({ - selectedState: 'loading', - }); - - expect(component.$el.querySelector('svg')).toBeDefined(); - expect(getTextFromNode(component, '.state-title')).toEqual(component.states.loading.title); - expect(getTextFromNode(component, '.state-description')).toEqual( - component.states.loading.description, - ); - - expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.loading.buttonText); - }); - - it('should show the unableToConnect state', () => { - const component = createComponent({ - selectedState: 'unableToConnect', - }); - - expect(component.$el.querySelector('svg')).toBeDefined(); - expect(getTextFromNode(component, '.state-title')).toEqual( - component.states.unableToConnect.title, - ); - - expect(component.$el.querySelector('.state-description a')).toBeDefined(); - expect(getTextFromNode(component, '.btn-success')).toEqual( - component.states.unableToConnect.buttonText, - ); - }); -}); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 82e42fe9ade..85e660d3925 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -922,3 +922,88 @@ export const metricsDashboardResponse = { }, status: 'success', }; + +export const dashboardGitResponse = [ + { + path: 'config/prometheus/common_metrics.yml', + display_name: 'Common Metrics', + default: true, + }, + { + path: '.gitlab/dashboards/super.yml', + display_name: 'Custom Dashboard 1', + default: false, + }, +]; + +export const graphDataPrometheusQuery = { + title: 'Super Chart A2', + type: 'single-stat', + weight: 2, + metrics: [ + { + id: 'metric_a1', + metric_id: 2, + query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024', + unit: 'MB', + label: 'Total Consumption', + prometheus_endpoint_path: + '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', + }, + ], + queries: [ + { + metricId: null, + id: 'metric_a1', + metric_id: 2, + query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024', + unit: 'MB', + label: 'Total Consumption', + prometheus_endpoint_path: + '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', + result: [ + { + metric: { job: 'prometheus' }, + value: ['2019-06-26T21:03:20.881Z', 91], + }, + ], + }, + ], +}; + +export const graphDataPrometheusQueryRange = { + title: 'Super Chart A1', + type: 'area', + weight: 2, + metrics: [ + { + id: 'metric_a1', + metric_id: 2, + query_range: + 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', + unit: 'MB', + label: 'Total Consumption', + prometheus_endpoint_path: + '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', + }, + ], + queries: [ + { + metricId: null, + id: 'metric_a1', + metric_id: 2, + query_range: + 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', + unit: 'MB', + label: 'Total Consumption', + prometheus_endpoint_path: + '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', + result: [ + { + metric: {}, + values: [[1495700554.925, '8.0390625'], [1495700614.925, '8.0390625']], + }, + ], + }, + ], +}; diff --git a/spec/javascripts/monitoring/panel_type_spec.js b/spec/javascripts/monitoring/panel_type_spec.js new file mode 100644 index 00000000000..086be628093 --- /dev/null +++ b/spec/javascripts/monitoring/panel_type_spec.js @@ -0,0 +1,78 @@ +import { shallowMount } from '@vue/test-utils'; +import PanelType from '~/monitoring/components/panel_type.vue'; +import EmptyChart from '~/monitoring/components/charts/empty_chart.vue'; +import AreaChart from '~/monitoring/components/charts/area.vue'; +import { graphDataPrometheusQueryRange } from './mock_data'; +import { createStore } from '~/monitoring/stores'; + +describe('Panel Type component', () => { + let store; + let panelType; + const dashboardWidth = 100; + + describe('When no graphData is available', () => { + let glEmptyChart; + // Deep clone object before modifying + const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange)); + graphDataNoResult.queries[0].result = []; + + beforeEach(() => { + panelType = shallowMount(PanelType, { + propsData: { + clipboardText: 'dashboard_link', + dashboardWidth, + graphData: graphDataNoResult, + }, + }); + }); + + afterEach(() => { + panelType.destroy(); + }); + + describe('Empty Chart component', () => { + beforeEach(() => { + glEmptyChart = panelType.find(EmptyChart); + }); + + it('is a Vue instance', () => { + expect(glEmptyChart.isVueInstance()).toBe(true); + }); + + it('it receives a graph title', () => { + const props = glEmptyChart.props(); + + expect(props.graphTitle).toBe(panelType.vm.graphData.title); + }); + }); + }); + + describe('when Graph data is available', () => { + const exampleText = 'example_text'; + + beforeEach(() => { + store = createStore(); + panelType = shallowMount(PanelType, { + propsData: { + clipboardText: exampleText, + dashboardWidth, + graphData: graphDataPrometheusQueryRange, + }, + store, + }); + }); + + describe('Area Chart panel type', () => { + it('is rendered', () => { + expect(panelType.find(AreaChart).exists()).toBe(true); + }); + + it('sets clipboard text on the dropdown', () => { + const link = () => panelType.find('.js-chart-link'); + const clipboardText = () => link().element.dataset.clipboardText; + + expect(clipboardText()).toBe(exampleText); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/store/actions_spec.js b/spec/javascripts/monitoring/store/actions_spec.js index 8c02e21eda2..955a39e03a5 100644 --- a/spec/javascripts/monitoring/store/actions_spec.js +++ b/spec/javascripts/monitoring/store/actions_spec.js @@ -22,6 +22,7 @@ import { environmentData, metricsDashboardResponse, metricsGroupsAPIResponse, + dashboardGitResponse, } from '../mock_data'; describe('Monitoring store actions', () => { @@ -51,9 +52,9 @@ describe('Monitoring store actions', () => { it('commits RECEIVE_DEPLOYMENTS_DATA_SUCCESS on error', done => { const dispatch = jasmine.createSpy(); const { state } = store; - state.deploymentEndpoint = '/success'; + state.deploymentsEndpoint = '/success'; - mock.onGet(state.deploymentEndpoint).reply(200, { + mock.onGet(state.deploymentsEndpoint).reply(200, { deployments: deploymentData, }); @@ -68,9 +69,9 @@ describe('Monitoring store actions', () => { it('commits RECEIVE_DEPLOYMENTS_DATA_FAILURE on error', done => { const dispatch = jasmine.createSpy(); const { state } = store; - state.deploymentEndpoint = '/error'; + state.deploymentsEndpoint = '/error'; - mock.onGet(state.deploymentEndpoint).reply(500); + mock.onGet(state.deploymentsEndpoint).reply(500); fetchDeploymentsData({ state, dispatch }) .then(() => { @@ -212,17 +213,19 @@ describe('Monitoring store actions', () => { describe('receiveMetricsDashboardSuccess', () => { let commit; let dispatch; + let state; beforeEach(() => { commit = jasmine.createSpy(); dispatch = jasmine.createSpy(); + state = storeState(); }); it('stores groups ', () => { const params = {}; const response = metricsDashboardResponse; - receiveMetricsDashboardSuccess({ commit, dispatch }, { response, params }); + receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response, params }); expect(commit).toHaveBeenCalledWith( types.RECEIVE_METRICS_DATA_SUCCESS, @@ -231,6 +234,18 @@ describe('Monitoring store actions', () => { expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetrics', params); }); + + it('sets the dashboards loaded from the repository', () => { + const params = {}; + const response = metricsDashboardResponse; + + response.all_dashboards = dashboardGitResponse; + state.multipleDashboardsEnabled = true; + + receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response, params }); + + expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse); + }); }); describe('receiveMetricsDashboardFailure', () => { @@ -298,8 +313,8 @@ describe('Monitoring store actions', () => { it('commits prometheus query result', done => { const commit = jasmine.createSpy(); const params = { - start: '1557216349.469', - end: '1557218149.469', + start: '2019-08-06T12:40:02.184Z', + end: '2019-08-06T20:40:02.184Z', }; const metric = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics[0]; const state = storeState(); diff --git a/spec/javascripts/monitoring/store/mutations_spec.js b/spec/javascripts/monitoring/store/mutations_spec.js index 02ff5847b34..43776b1b7f2 100644 --- a/spec/javascripts/monitoring/store/mutations_spec.js +++ b/spec/javascripts/monitoring/store/mutations_spec.js @@ -1,7 +1,12 @@ import mutations from '~/monitoring/stores/mutations'; import * as types from '~/monitoring/stores/mutation_types'; import state from '~/monitoring/stores/state'; -import { metricsGroupsAPIResponse, deploymentData, metricsDashboardResponse } from '../mock_data'; +import { + metricsGroupsAPIResponse, + deploymentData, + metricsDashboardResponse, + dashboardGitResponse, +} from '../mock_data'; describe('Monitoring mutations', () => { let stateCopy; @@ -110,12 +115,14 @@ describe('Monitoring mutations', () => { environmentsEndpoint: 'environments.json', deploymentsEndpoint: 'deployments.json', dashboardEndpoint: 'dashboard.json', + projectPath: '/gitlab-org/gitlab-ce', }); expect(stateCopy.metricsEndpoint).toEqual('additional_metrics.json'); expect(stateCopy.environmentsEndpoint).toEqual('environments.json'); expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json'); expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json'); + expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-ce'); }); }); @@ -156,4 +163,12 @@ describe('Monitoring mutations', () => { expect(stateCopy.metricsWithData).toEqual([]); }); }); + + describe('SET_ALL_DASHBOARDS', () => { + it('stores the dashboards loaded from the git repository', () => { + mutations[types.SET_ALL_DASHBOARDS](stateCopy, dashboardGitResponse); + + expect(stateCopy.allDashboards).toEqual(dashboardGitResponse); + }); + }); }); diff --git a/spec/javascripts/monitoring/store/utils_spec.js b/spec/javascripts/monitoring/store/utils_spec.js new file mode 100644 index 00000000000..73dd370ffb3 --- /dev/null +++ b/spec/javascripts/monitoring/store/utils_spec.js @@ -0,0 +1,37 @@ +import { groupQueriesByChartInfo } from '~/monitoring/stores/utils'; + +describe('groupQueriesByChartInfo', () => { + let input; + let output; + + it('groups metrics with the same chart title and y_axis label', () => { + input = [ + { title: 'title', y_label: 'MB', queries: [{}] }, + { title: 'title', y_label: 'MB', queries: [{}] }, + { title: 'new title', y_label: 'MB', queries: [{}] }, + ]; + + output = [ + { title: 'title', y_label: 'MB', queries: [{ metricId: null }, { metricId: null }] }, + { title: 'new title', y_label: 'MB', queries: [{ metricId: null }] }, + ]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); + + // Functionality associated with the /additional_metrics endpoint + it("associates a chart's stringified metric_id with the metric", () => { + input = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{}] }]; + output = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{ metricId: '3' }] }]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); + + // Functionality associated with the /metrics_dashboard endpoint + it('aliases a stringified metrics_id on the metric to the metricId key', () => { + input = [{ title: 'new title', y_label: 'MB', queries: [{ metric_id: 3 }] }]; + output = [{ title: 'new title', y_label: 'MB', queries: [{ metricId: '3', metric_id: 3 }] }]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); +}); diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js index e3c455d1686..e22e8cdc03d 100644 --- a/spec/javascripts/monitoring/utils_spec.js +++ b/spec/javascripts/monitoring/utils_spec.js @@ -1,29 +1,64 @@ -import { getTimeDiff } from '~/monitoring/utils'; +import { getTimeDiff, graphDataValidatorForValues } from '~/monitoring/utils'; import { timeWindows } from '~/monitoring/constants'; +import { graphDataPrometheusQuery, graphDataPrometheusQueryRange } from './mock_data'; describe('getTimeDiff', () => { + function secondsBetween({ start, end }) { + return (new Date(end) - new Date(start)) / 1000; + } + + function minutesBetween(timeRange) { + return secondsBetween(timeRange) / 60; + } + + function hoursBetween(timeRange) { + return minutesBetween(timeRange) / 60; + } + it('defaults to an 8 hour (28800s) difference', () => { const params = getTimeDiff(); - expect(params.end - params.start).toEqual(28800); + expect(hoursBetween(params)).toEqual(8); }); it('accepts time window as an argument', () => { - const params = getTimeDiff(timeWindows.thirtyMinutes); + const params = getTimeDiff('thirtyMinutes'); - expect(params.end - params.start).not.toEqual(28800); + expect(minutesBetween(params)).toEqual(30); }); it('returns a value for every defined time window', () => { const nonDefaultWindows = Object.keys(timeWindows).filter(window => window !== 'eightHours'); - nonDefaultWindows.forEach(window => { - const params = getTimeDiff(timeWindows[window]); - const diff = params.end - params.start; + nonDefaultWindows.forEach(timeWindow => { + const params = getTimeDiff(timeWindow); - // Ensure we're not returning the default, 28800 (the # of seconds in 8 hrs) - expect(diff).not.toEqual(28800); - expect(typeof diff).toEqual('number'); + // Ensure we're not returning the default + expect(hoursBetween(params)).not.toEqual(8); }); }); }); + +describe('graphDataValidatorForValues', () => { + /* + * When dealing with a metric using the query format, e.g. + * query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024' + * the validator will look for the `value` key instead of `values` + */ + it('validates data with the query format', () => { + const validGraphData = graphDataValidatorForValues(true, graphDataPrometheusQuery); + + expect(validGraphData).toBe(true); + }); + + /* + * When dealing with a metric using the query?range format, e.g. + * query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', + * the validator will look for the `values` key instead of `value` + */ + it('validates data with the query_range format', () => { + const validGraphData = graphDataValidatorForValues(false, graphDataPrometheusQueryRange); + + expect(validGraphData).toBe(true); + }); +}); diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index 362963ddaf4..88c86746992 100644 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -251,6 +251,21 @@ describe('issue_comment_form component', () => { }); }); }); + + describe('when toggling state', () => { + it('should update MR count', done => { + spyOn(vm, 'closeIssue').and.returnValue(Promise.resolve()); + + const updateMrCountSpy = spyOnDependency(CommentForm, 'refreshUserMergeRequestCounts'); + vm.toggleIssueState(); + + Vue.nextTick(() => { + expect(updateMrCountSpy).toHaveBeenCalled(); + + done(); + }); + }); + }); }); describe('issue is confidential', () => { diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js index 0752bd05904..f849fe9d8bb 100644 --- a/spec/javascripts/notes/components/diff_with_note_spec.js +++ b/spec/javascripts/notes/components/diff_with_note_spec.js @@ -47,6 +47,19 @@ describe('diff_with_note', () => { vm = mountComponentWithStore(Component, { props, store }); }); + it('removes trailing "+" char', () => { + const richText = vm.$el.querySelectorAll('.line_holder')[4].querySelector('.line_content') + .textContent[0]; + + expect(richText).not.toEqual('+'); + }); + + it('removes trailing "-" char', () => { + const richText = vm.$el.querySelector('#LC13').parentNode.textContent[0]; + + expect(richText).not.toEqual('-'); + }); + it('shows text diff', () => { expect(selectors.container).toHaveClass('text-file'); expect(selectors.diffTable).toExist(); diff --git a/spec/javascripts/notes/components/note_actions/reply_button_spec.js b/spec/javascripts/notes/components/note_actions/reply_button_spec.js index 11fb89808d9..003773d07ea 100644 --- a/spec/javascripts/notes/components/note_actions/reply_button_spec.js +++ b/spec/javascripts/notes/components/note_actions/reply_button_spec.js @@ -25,8 +25,7 @@ describe('ReplyButton', () => { button.trigger('click'); - expect(wrapper.emitted()).toEqual({ - startReplying: [[]], - }); + expect(wrapper.emitted().startReplying).toBeTruthy(); + expect(wrapper.emitted().startReplying.length).toBe(1); }); }); diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index efa864e7d00..74805ca8c00 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -36,14 +36,20 @@ describe('noteable_discussion component', () => { }); it('should render user avatar', () => { + const discussion = { ...discussionMock }; + discussion.diff_file = mockDiffFile; + discussion.diff_discussion = true; + + wrapper.setProps({ discussion, renderDiffFile: true }); + expect(wrapper.find('.user-avatar-link').exists()).toBe(true); }); - it('should not render discussion header for non diff discussions', () => { + it('should not render thread header for non diff threads', () => { expect(wrapper.find('.discussion-header').exists()).toBe(false); }); - it('should render discussion header', done => { + it('should render thread header', done => { const discussion = { ...discussionMock }; discussion.diff_file = mockDiffFile; discussion.diff_discussion = true; @@ -90,16 +96,16 @@ describe('noteable_discussion component', () => { .catch(done.fail); }); - it('does not render jump to discussion button', () => { - expect( - wrapper.find('*[data-original-title="Jump to next unresolved discussion"]').exists(), - ).toBe(false); + it('does not render jump to thread button', () => { + expect(wrapper.find('*[data-original-title="Jump to next unresolved thread"]').exists()).toBe( + false, + ); }); }); describe('methods', () => { describe('jumpToNextDiscussion', () => { - it('expands next unresolved discussion', done => { + it('expands next unresolved thread', done => { const discussion2 = getJSONFixture(discussionWithTwoUnresolvedNotes)[0]; discussion2.resolved = false; discussion2.active = true; @@ -114,9 +120,7 @@ describe('noteable_discussion component', () => { const nextDiscussionId = discussion2.id; - setFixtures(` - <div class="discussion" data-discussion-id="${nextDiscussionId}"></div> - `); + setFixtures(`<div class="discussion" data-discussion-id="${nextDiscussionId}"></div>`); wrapper.vm.jumpToNextDiscussion(); @@ -162,20 +166,20 @@ describe('noteable_discussion component', () => { .catch(done.fail); }); - describe('for commit discussions', () => { - it('should display a monospace started a discussion on commit', () => { - expect(wrapper.text()).toContain(`started a discussion on commit ${truncatedCommitId}`); + describe('for commit threads', () => { + it('should display a monospace started a thread on commit', () => { + expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`); expect(commitElement.exists()).toBe(true); expect(commitElement.text()).toContain(truncatedCommitId); }); }); - describe('for diff discussion with a commit id', () => { - it('should display started discussion on commit header', done => { + describe('for diff thread with a commit id', () => { + it('should display started thread on commit header', done => { wrapper.vm.discussion.for_commit = false; wrapper.vm.$nextTick(() => { - expect(wrapper.text()).toContain(`started a discussion on commit ${truncatedCommitId}`); + expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`); expect(commitElement).not.toBe(null); @@ -189,7 +193,7 @@ describe('noteable_discussion component', () => { wrapper.vm.$nextTick(() => { expect(wrapper.text()).toContain( - `started a discussion on an outdated change in commit ${truncatedCommitId}`, + `started a thread on an outdated change in commit ${truncatedCommitId}`, ); expect(commitElement).not.toBe(null); @@ -199,21 +203,21 @@ describe('noteable_discussion component', () => { }); }); - describe('for diff discussions without a commit id', () => { - it('should show started a discussion on the diff text', done => { + describe('for diff threads without a commit id', () => { + it('should show started a thread on the diff text', done => { Object.assign(wrapper.vm.discussion, { for_commit: false, commit_id: null, }); wrapper.vm.$nextTick(() => { - expect(wrapper.text()).toContain('started a discussion on the diff'); + expect(wrapper.text()).toContain('started a thread on the diff'); done(); }); }); - it('should show discussion on older version text', done => { + it('should show thread on older version text', done => { Object.assign(wrapper.vm.discussion, { for_commit: false, commit_id: null, @@ -221,7 +225,7 @@ describe('noteable_discussion component', () => { }); wrapper.vm.$nextTick(() => { - expect(wrapper.text()).toContain('started a discussion on an old version of the diff'); + expect(wrapper.text()).toContain('started a thread on an old version of the diff'); done(); }); @@ -229,7 +233,7 @@ describe('noteable_discussion component', () => { }); }); - describe('for resolved discussion', () => { + describe('for resolved thread', () => { beforeEach(() => { const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0]; wrapper.setProps({ discussion }); @@ -242,7 +246,7 @@ describe('noteable_discussion component', () => { }); }); - describe('for unresolved discussion', () => { + describe('for unresolved thread', () => { beforeEach(done => { const discussion = { ...getJSONFixture(discussionWithTwoUnresolvedNotes)[0], diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 1df5cf9ef68..5f81a168498 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -1,3 +1,5 @@ +// Copied to ee/spec/frontend/notes/mock_data.js + export const notesDataMock = { discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json', lastFetchedAt: 1501862675, diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 7a9f32ddcff..e55aa0e965a 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import $ from 'jquery'; import _ from 'underscore'; +import Api from '~/api'; import { TEST_HOST } from 'spec/test_constants'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import actionsModule, * as actions from '~/notes/stores/actions'; @@ -8,7 +9,6 @@ import * as mutationTypes from '~/notes/stores/mutation_types'; import * as notesConstants from '~/notes/constants'; import createStore from '~/notes/stores'; import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; -import service from '~/notes/services/notes_service'; import testAction from '../../helpers/vuex_action_helper'; import { resetStore } from '../helpers'; import { @@ -18,6 +18,8 @@ import { noteableDataMock, individualNote, } from '../mock_data'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; const TEST_ERROR_MESSAGE = 'Test error message'; @@ -335,28 +337,24 @@ describe('Actions Notes Store', () => { }); describe('deleteNote', () => { - const interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify({}), { - status: 200, - }), - ); - }; + const endpoint = `${TEST_HOST}/note`; + let axiosMock; beforeEach(() => { - Vue.http.interceptors.push(interceptor); + axiosMock = new AxiosMockAdapter(axios); + axiosMock.onDelete(endpoint).replyOnce(200, {}); $('body').attr('data-page', ''); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + axiosMock.restore(); $('body').attr('data-page', ''); }); it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', done => { - const note = { path: `${gl.TEST_HOST}`, id: 1 }; + const note = { path: endpoint, id: 1 }; testAction( actions.deleteNote, @@ -373,7 +371,7 @@ describe('Actions Notes Store', () => { type: 'updateMergeRequestWidget', }, { - type: 'updateResolvableDiscussonsCounts', + type: 'updateResolvableDiscussionsCounts', }, ], done, @@ -381,7 +379,7 @@ describe('Actions Notes Store', () => { }); it('dispatches removeDiscussionsFromDiff on merge request page', done => { - const note = { path: `${gl.TEST_HOST}`, id: 1 }; + const note = { path: endpoint, id: 1 }; $('body').attr('data-page', 'projects:merge_requests:show'); @@ -400,7 +398,7 @@ describe('Actions Notes Store', () => { type: 'updateMergeRequestWidget', }, { - type: 'updateResolvableDiscussonsCounts', + type: 'updateResolvableDiscussionsCounts', }, { type: 'diffs/removeDiscussionsFromDiff', @@ -452,7 +450,7 @@ describe('Actions Notes Store', () => { type: 'startTaskList', }, { - type: 'updateResolvableDiscussonsCounts', + type: 'updateResolvableDiscussionsCounts', }, ], done, @@ -527,7 +525,7 @@ describe('Actions Notes Store', () => { ], [ { - type: 'updateResolvableDiscussonsCounts', + type: 'updateResolvableDiscussionsCounts', }, { type: 'updateMergeRequestWidget', @@ -552,7 +550,7 @@ describe('Actions Notes Store', () => { ], [ { - type: 'updateResolvableDiscussonsCounts', + type: 'updateResolvableDiscussionsCounts', }, { type: 'updateMergeRequestWidget', @@ -587,10 +585,10 @@ describe('Actions Notes Store', () => { }); }); - describe('updateResolvableDiscussonsCounts', () => { + describe('updateResolvableDiscussionsCounts', () => { it('commits UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', done => { testAction( - actions.updateResolvableDiscussonsCounts, + actions.updateResolvableDiscussionsCounts, null, {}, [{ type: 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS' }], @@ -712,7 +710,7 @@ describe('Actions Notes Store', () => { [ { type: 'updateMergeRequestWidget' }, { type: 'startTaskList' }, - { type: 'updateResolvableDiscussonsCounts' }, + { type: 'updateResolvableDiscussionsCounts' }, ], done, ); @@ -846,9 +844,9 @@ describe('Actions Notes Store', () => { let flashContainer; beforeEach(() => { - spyOn(service, 'applySuggestion'); + spyOn(Api, 'applySuggestion'); dispatch.and.returnValue(Promise.resolve()); - service.applySuggestion.and.returnValue(Promise.resolve()); + Api.applySuggestion.and.returnValue(Promise.resolve()); flashContainer = {}; }); @@ -877,7 +875,7 @@ describe('Actions Notes Store', () => { it('when service fails, flashes error message', done => { const response = { response: { data: { message: TEST_ERROR_MESSAGE } } }; - service.applySuggestion.and.returnValue(Promise.reject(response)); + Api.applySuggestion.and.returnValue(Promise.reject(response)); testSubmitSuggestion(done, () => { expect(commit).not.toHaveBeenCalled(); @@ -894,4 +892,31 @@ describe('Actions Notes Store', () => { }); }); }); + + describe('filterDiscussion', () => { + const path = 'some-discussion-path'; + const filter = 0; + + beforeEach(() => { + dispatch.and.returnValue(new Promise(() => {})); + }); + + it('fetches discussions with filter and persistFilter false', () => { + actions.filterDiscussion({ dispatch }, { path, filter, persistFilter: false }); + + expect(dispatch.calls.allArgs()).toEqual([ + ['setLoadingState', true], + ['fetchDiscussions', { path, filter, persistFilter: false }], + ]); + }); + + it('fetches discussions with filter and persistFilter true', () => { + actions.filterDiscussion({ dispatch }, { path, filter, persistFilter: true }); + + expect(dispatch.calls.allArgs()).toEqual([ + ['setLoadingState', true], + ['fetchDiscussions', { path, filter, persistFilter: true }], + ]); + }); + }); }); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index 8f3c493dd4c..71dcba114a9 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -32,6 +32,26 @@ describe('Getters Notes Store', () => { }; }); + describe('showJumpToNextDiscussion', () => { + it('should return true if there are 2 or more unresolved discussions', () => { + const localGetters = { + unresolvedDiscussionsIdsByDate: ['123', '456'], + allResolvableDiscussions: [], + }; + + expect(getters.showJumpToNextDiscussion(state, localGetters)()).toBe(true); + }); + + it('should return false if there are 1 or less unresolved discussions', () => { + const localGetters = { + unresolvedDiscussionsIdsByDate: ['123'], + allResolvableDiscussions: [], + }; + + expect(getters.showJumpToNextDiscussion(state, localGetters)()).toBe(false); + }); + }); + describe('discussions', () => { it('should return all discussions in the store', () => { expect(getters.discussions(state)).toEqual([individualNote]); @@ -236,6 +256,54 @@ describe('Getters Notes Store', () => { }); }); + describe('previousUnresolvedDiscussionId', () => { + describe('with unresolved discussions', () => { + const localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], + }; + + it('with bogus returns falsey', () => { + expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('456'); + }); + + [ + { id: '123', expected: '789' }, + { id: '456', expected: '123' }, + { id: '789', expected: '456' }, + ].forEach(({ id, expected }) => { + it(`with ${id}, returns previous value`, () => { + expect(getters.previousUnresolvedDiscussionId(state, localGetters)(id)).toBe(expected); + }); + }); + }); + + describe('with 1 unresolved discussion', () => { + const localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123'], + }; + + it('with bogus returns id', () => { + expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('123'); + }); + + it('with match, returns value', () => { + expect(getters.previousUnresolvedDiscussionId(state, localGetters)('123')).toEqual('123'); + }); + }); + + describe('with 0 unresolved discussions', () => { + const localGetters = { + unresolvedDiscussionsIdsOrdered: () => [], + }; + + it('returns undefined', () => { + expect( + getters.previousUnresolvedDiscussionId(state, localGetters)('bogus'), + ).toBeUndefined(); + }); + }); + }); + describe('firstUnresolvedDiscussionId', () => { const localGetters = { unresolvedDiscussionsIdsByDate: ['123', '456'], diff --git a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js index 08a8362797b..75912612255 100644 --- a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js +++ b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js @@ -26,9 +26,7 @@ describe('Promote label modal', () => { it('contains the proper description', () => { expect(vm.text).toContain( - `Promoting ${labelMockData.labelTitle} will make it available for all projects inside ${ - labelMockData.groupName - }`, + `Promoting ${labelMockData.labelTitle} will make it available for all projects inside ${labelMockData.groupName}`, ); }); diff --git a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js index 2ac73ef3024..3d25a278cef 100644 --- a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js +++ b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js @@ -24,9 +24,7 @@ describe('Promote milestone modal', () => { it('contains the proper description', () => { expect(vm.text).toContain( - `Promoting ${ - milestoneMockData.milestoneTitle - } will make it available for all projects inside ${milestoneMockData.groupName}.`, + `Promoting ${milestoneMockData.milestoneTitle} will make it available for all projects inside ${milestoneMockData.groupName}.`, ); }); diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js index 6dea570266b..efeb65acf87 100644 --- a/spec/javascripts/pdf/page_spec.js +++ b/spec/javascripts/pdf/page_spec.js @@ -17,7 +17,7 @@ describe('Page component', () => { pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc; pdfjsLib .getDocument(testPDF) - .then(pdf => pdf.getPage(1)) + .promise.then(pdf => pdf.getPage(1)) .then(page => { testPage = page; }) @@ -31,7 +31,8 @@ describe('Page component', () => { it('renders the page when mounting', done => { const promise = Promise.resolve(); - spyOn(testPage, 'render').and.callFake(() => promise); + spyOn(testPage, 'render').and.returnValue({ promise }); + vm = mountComponent(Component, { page: testPage, number: 1, diff --git a/spec/javascripts/performance_bar/components/detailed_metric_spec.js b/spec/javascripts/performance_bar/components/detailed_metric_spec.js index 8a7aa057186..0486b5fa3db 100644 --- a/spec/javascripts/performance_bar/components/detailed_metric_spec.js +++ b/spec/javascripts/performance_bar/components/detailed_metric_spec.js @@ -44,7 +44,6 @@ describe('detailedMetric', () => { }, metric: 'gitaly', header: 'Gitaly calls', - details: 'details', keys: ['feature', 'request'], }); }); @@ -79,8 +78,32 @@ describe('detailedMetric', () => { }); }); - it('displays the metric name', () => { + it('displays the metric title', () => { expect(vm.$el.innerText).toContain('gitaly'); }); + + describe('when using a custom metric title', () => { + beforeEach(() => { + vm = mountComponent(Vue.extend(detailedMetric), { + currentRequest: { + details: { + gitaly: { + duration: '123ms', + calls: '456', + details: requestDetails, + }, + }, + }, + metric: 'gitaly', + title: 'custom', + header: 'Gitaly calls', + keys: ['feature', 'request'], + }); + }); + + it('displays the custom title', () => { + expect(vm.$el.innerText).toContain('custom'); + }); + }); }); }); diff --git a/spec/javascripts/performance_bar/components/simple_metric_spec.js b/spec/javascripts/performance_bar/components/simple_metric_spec.js deleted file mode 100644 index 98b843e9711..00000000000 --- a/spec/javascripts/performance_bar/components/simple_metric_spec.js +++ /dev/null @@ -1,47 +0,0 @@ -import Vue from 'vue'; -import simpleMetric from '~/performance_bar/components/simple_metric.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('simpleMetric', () => { - let vm; - - afterEach(() => { - vm.$destroy(); - }); - - describe('when the current request has no details', () => { - beforeEach(() => { - vm = mountComponent(Vue.extend(simpleMetric), { - currentRequest: {}, - metric: 'gitaly', - }); - }); - - it('does not display details', () => { - expect(vm.$el.innerText).not.toContain('/'); - }); - - it('displays the metric name', () => { - expect(vm.$el.innerText).toContain('gitaly'); - }); - }); - - describe('when the current request has details', () => { - beforeEach(() => { - vm = mountComponent(Vue.extend(simpleMetric), { - currentRequest: { - details: { gitaly: { duration: '123ms', calls: '456' } }, - }, - metric: 'gitaly', - }); - }); - - it('diplays details', () => { - expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456'); - }); - - it('displays the metric name', () => { - expect(vm.$el.innerText).toContain('gitaly'); - }); - }); -}); diff --git a/spec/javascripts/persistent_user_callout_spec.js b/spec/javascripts/persistent_user_callout_spec.js index 2fdfff3db03..d15758be5d2 100644 --- a/spec/javascripts/persistent_user_callout_spec.js +++ b/spec/javascripts/persistent_user_callout_spec.js @@ -22,6 +22,24 @@ describe('PersistentUserCallout', () => { return fixture; } + function createDeferredLinkFixture() { + const fixture = document.createElement('div'); + fixture.innerHTML = ` + <div + class="container" + data-dismiss-endpoint="${dismissEndpoint}" + data-feature-id="${featureName}" + data-defer-links="true" + > + <button type="button" class="js-close"></button> + <a href="/somewhere-pleasant" target="_blank" class="deferred-link">A link</a> + <a href="/somewhere-else" target="_blank" class="normal-link">Another link</a> + </div> + `; + + return fixture; + } + describe('dismiss', () => { let button; let mockAxios; @@ -74,6 +92,75 @@ describe('PersistentUserCallout', () => { }); }); + describe('deferred links', () => { + let button; + let deferredLink; + let normalLink; + let mockAxios; + let persistentUserCallout; + let windowSpy; + + beforeEach(() => { + const fixture = createDeferredLinkFixture(); + const container = fixture.querySelector('.container'); + button = fixture.querySelector('.js-close'); + deferredLink = fixture.querySelector('.deferred-link'); + normalLink = fixture.querySelector('.normal-link'); + mockAxios = new MockAdapter(axios); + persistentUserCallout = new PersistentUserCallout(container); + spyOn(persistentUserCallout.container, 'remove'); + windowSpy = spyOn(window, 'open').and.callFake(() => {}); + }); + + afterEach(() => { + mockAxios.restore(); + }); + + it('defers loading of a link until callout is dismissed', done => { + const { href, target } = deferredLink; + mockAxios.onPost(dismissEndpoint).replyOnce(200); + + deferredLink.click(); + + setTimeoutPromise() + .then(() => { + expect(windowSpy).toHaveBeenCalledWith(href, target); + expect(persistentUserCallout.container.remove).toHaveBeenCalled(); + expect(mockAxios.history.post[0].data).toBe( + JSON.stringify({ feature_name: featureName }), + ); + }) + .then(done) + .catch(done.fail); + }); + + it('does not dismiss callout on non-deferred links', done => { + normalLink.click(); + + setTimeoutPromise() + .then(() => { + expect(windowSpy).not.toHaveBeenCalled(); + expect(persistentUserCallout.container.remove).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('does not follow link when notification is closed', done => { + mockAxios.onPost(dismissEndpoint).replyOnce(200); + + button.click(); + + setTimeoutPromise() + .then(() => { + expect(windowSpy).not.toHaveBeenCalled(); + expect(persistentUserCallout.container.remove).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('factory', () => { it('returns an instance of PersistentUserCallout with the provided container property', () => { const fixture = createFixture(); diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js index 8eef9166b8d..03ead6cd8ba 100644 --- a/spec/javascripts/pipelines/mock_data.js +++ b/spec/javascripts/pipelines/mock_data.js @@ -1,6 +1,5 @@ export const pipelineWithStages = { id: 20333396, - iid: 304399, user: { id: 128633, name: 'Rémy Coutable', diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 88c0137dc58..aa196af2f33 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -13,7 +13,6 @@ describe('Pipeline Url Component', () => { propsData: { pipeline: { id: 1, - iid: 1, path: 'foo', flags: {}, }, @@ -29,7 +28,6 @@ describe('Pipeline Url Component', () => { propsData: { pipeline: { id: 1, - iid: 1, path: 'foo', flags: {}, }, @@ -49,7 +47,6 @@ describe('Pipeline Url Component', () => { propsData: { pipeline: { id: 1, - iid: 1, path: 'foo', flags: { latest: true, @@ -81,7 +78,6 @@ describe('Pipeline Url Component', () => { propsData: { pipeline: { id: 1, - iid: 1, path: 'foo', flags: { latest: true, @@ -104,7 +100,6 @@ describe('Pipeline Url Component', () => { propsData: { pipeline: { id: 1, - iid: 1, path: 'foo', flags: { failure_reason: true, diff --git a/spec/javascripts/pipelines/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js index a7dcd532f4f..953a42b9d15 100644 --- a/spec/javascripts/pipelines/pipelines_actions_spec.js +++ b/spec/javascripts/pipelines/pipelines_actions_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; -import eventHub from '~/pipelines/event_hub'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import PipelinesActions from '~/pipelines/components/pipelines_actions.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { TEST_HOST } from 'spec/test_constants'; @@ -7,9 +8,15 @@ import { TEST_HOST } from 'spec/test_constants'; describe('Pipelines Actions dropdown', () => { const Component = Vue.extend(PipelinesActions); let vm; + let mock; afterEach(() => { vm.$destroy(); + mock.restore(); + }); + + beforeEach(() => { + mock = new MockAdapter(axios); }); describe('manual actions', () => { @@ -40,6 +47,22 @@ describe('Pipelines Actions dropdown', () => { expect(dropdownItem).toBeDisabled(); }); + + describe('on click', () => { + it('makes a request and toggles the loading state', done => { + mock.onPost(actions.path).reply(200); + + vm.$el.querySelector('.dropdown-menu li button').click(); + + expect(vm.isLoading).toEqual(true); + + setTimeout(() => { + expect(vm.isLoading).toEqual(false); + + done(); + }); + }); + }); }); describe('scheduled jobs', () => { @@ -71,26 +94,27 @@ describe('Pipelines Actions dropdown', () => { .catch(done.fail); }); - it('emits postAction event after confirming', () => { - const emitSpy = jasmine.createSpy('emit'); - eventHub.$on('postAction', emitSpy); + it('makes post request after confirming', done => { + mock.onPost(scheduledJobAction.path).reply(200); spyOn(window, 'confirm').and.callFake(() => true); findDropdownItem(scheduledJobAction).click(); expect(window.confirm).toHaveBeenCalled(); - expect(emitSpy).toHaveBeenCalledWith(scheduledJobAction.path); + setTimeout(() => { + expect(mock.history.post.length).toBe(1); + done(); + }); }); - it('does not emit postAction event if confirmation is cancelled', () => { - const emitSpy = jasmine.createSpy('emit'); - eventHub.$on('postAction', emitSpy); + it('does not make post request if confirmation is cancelled', () => { + mock.onPost(scheduledJobAction.path).reply(200); spyOn(window, 'confirm').and.callFake(() => false); findDropdownItem(scheduledJobAction).click(); expect(window.confirm).toHaveBeenCalled(); - expect(emitSpy).not.toHaveBeenCalled(); + expect(mock.history.post.length).toBe(0); }); it('displays the remaining time in the dropdown', () => { diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 78187b69563..daa898ca687 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -736,10 +736,9 @@ describe('Pipelines', () => { }); describe('when a request is being made', () => { - it('stops polling, cancels the request, fetches pipelines & restarts polling', done => { + it('stops polling, cancels the request, & restarts polling', done => { spyOn(vm.poll, 'stop'); spyOn(vm.poll, 'restart'); - spyOn(vm, 'getPipelines').and.returnValue(Promise.resolve()); spyOn(vm.service.cancelationSource, 'cancel').and.callThrough(); setTimeout(() => { @@ -754,7 +753,6 @@ describe('Pipelines', () => { expect(vm.poll.stop).toHaveBeenCalled(); setTimeout(() => { - expect(vm.getPipelines).toHaveBeenCalled(); expect(vm.poll.restart).toHaveBeenCalled(); done(); }, 0); @@ -765,10 +763,9 @@ describe('Pipelines', () => { }); describe('when no request is being made', () => { - it('stops polling, fetches pipelines & restarts polling', done => { + it('stops polling & restarts polling', done => { spyOn(vm.poll, 'stop'); spyOn(vm.poll, 'restart'); - spyOn(vm, 'getPipelines').and.returnValue(Promise.resolve()); setTimeout(() => { vm.$el.querySelector('.js-builds-dropdown-button').click(); @@ -776,7 +773,6 @@ describe('Pipelines', () => { expect(vm.poll.stop).toHaveBeenCalled(); setTimeout(() => { - expect(vm.getPipelines).toHaveBeenCalled(); expect(vm.poll.restart).toHaveBeenCalled(); done(); }, 0); diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 76a17e6fb31..e7675669f7a 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -8,6 +8,13 @@ import { reposServerResponse } from '../mock_data'; describe('Registry List', () => { const Component = Vue.extend(registry); + const props = { + endpoint: `${TEST_HOST}/foo`, + helpPagePath: 'foo', + noContainersImage: 'foo', + containersErrorImage: 'foo', + repositoryUrl: 'foo', + }; let vm; let mock; @@ -24,7 +31,7 @@ describe('Registry List', () => { beforeEach(() => { mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, reposServerResponse); - vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` }); + vm = mountComponent(Component, { ...props }); }); it('should render a list of repos', done => { @@ -72,7 +79,7 @@ describe('Registry List', () => { beforeEach(() => { mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []); - vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` }); + vm = mountComponent(Component, { ...props }); }); it('should render empty message', done => { @@ -83,7 +90,7 @@ describe('Registry List', () => { .textContent.trim() .replace(/[\r\n]+/g, ' '), ).toEqual( - 'No container images stored for this project. Add one by following the instructions above.', + 'With the Container Registry, every project can have its own space to store its Docker images. More Information', ); done(); }, 0); @@ -94,12 +101,30 @@ describe('Registry List', () => { beforeEach(() => { mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []); - vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` }); + vm = mountComponent(Component, { ...props }); }); it('should render a loading spinner', done => { Vue.nextTick(() => { - expect(vm.$el.querySelector('.spinner')).not.toBe(null); + expect(vm.$el.querySelector('.gl-spinner')).not.toBe(null); + done(); + }); + }); + }); + + describe('invalid characters in path', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []); + + vm = mountComponent(Component, { + ...props, + characterError: true, + }); + }); + + it('should render invalid characters error message', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.container-message')).not.toBe(null); done(); }); }); diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js index a3f7ff76dc7..2a5d8dd11da 100644 --- a/spec/javascripts/registry/components/collapsible_container_spec.js +++ b/spec/javascripts/registry/components/collapsible_container_spec.js @@ -12,6 +12,8 @@ describe('collapsible registry container', () => { let mock; const Component = Vue.extend(collapsibleComponent); + const findDeleteBtn = () => vm.$el.querySelector('.js-remove-repo'); + beforeEach(() => { mock = new MockAdapter(axios); @@ -67,7 +69,19 @@ describe('collapsible registry container', () => { describe('delete repo', () => { it('should be possible to delete a repo', () => { - expect(vm.$el.querySelector('.js-remove-repo')).not.toBeNull(); + expect(findDeleteBtn()).not.toBeNull(); + }); + + it('should call deleteItem when confirming deletion', done => { + findDeleteBtn().click(); + spyOn(vm, 'deleteItem').and.returnValue(Promise.resolve()); + + Vue.nextTick(() => { + document.querySelector(`#${vm.modalId} .btn-danger`).click(); + + expect(vm.deleteItem).toHaveBeenCalledWith(vm.repo); + done(); + }); }); }); }); diff --git a/spec/javascripts/registry/components/table_registry_spec.js b/spec/javascripts/registry/components/table_registry_spec.js index 7f5252a7d6c..9c7439206ef 100644 --- a/spec/javascripts/registry/components/table_registry_spec.js +++ b/spec/javascripts/registry/components/table_registry_spec.js @@ -1,44 +1,161 @@ import Vue from 'vue'; import tableRegistry from '~/registry/components/table_registry.vue'; import store from '~/registry/stores'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { repoPropsData } from '../mock_data'; +const [firstImage, secondImage] = repoPropsData.list; + describe('table registry', () => { let vm; - let Component; + const Component = Vue.extend(tableRegistry); + const bulkDeletePath = 'path'; - beforeEach(() => { - Component = Vue.extend(tableRegistry); - vm = new Component({ + const findDeleteBtn = () => vm.$el.querySelector('.js-delete-registry'); + const findDeleteBtnRow = () => vm.$el.querySelector('.js-delete-registry-row'); + const findSelectAllCheckbox = () => vm.$el.querySelector('.js-select-all-checkbox > input'); + const findAllRowCheckboxes = () => + Array.from(vm.$el.querySelectorAll('.js-select-checkbox input')); + const confirmationModal = (child = '') => document.querySelector(`#${vm.modalId} ${child}`); + + const createComponent = () => { + vm = mountComponentWithStore(Component, { store, - propsData: { + props: { repo: repoPropsData, }, - }).$mount(); + }); + }; + + const selectAllCheckboxes = () => vm.selectAll(); + const deselectAllCheckboxes = () => vm.deselectAll(); + + beforeEach(() => { + createComponent(); }); afterEach(() => { vm.$destroy(); }); - it('should render a table with the registry list', () => { - expect(vm.$el.querySelectorAll('table tbody tr').length).toEqual(repoPropsData.list.length); + describe('rendering', () => { + it('should render a table with the registry list', () => { + expect(vm.$el.querySelectorAll('table tbody tr').length).toEqual(repoPropsData.list.length); + }); + + it('should render registry tag', () => { + const textRendered = vm.$el + .querySelector('.table tbody tr') + .textContent.trim() + // replace additional whitespace characters (e.g. new lines) with a single empty space + .replace(/\s\s+/g, ' '); + + expect(textRendered).toContain(repoPropsData.list[0].tag); + expect(textRendered).toContain(repoPropsData.list[0].shortRevision); + expect(textRendered).toContain(repoPropsData.list[0].layers); + expect(textRendered).toContain(repoPropsData.list[0].size); + }); }); - it('should render registry tag', () => { - const textRendered = vm.$el - .querySelector('.table tbody tr') - .textContent.trim() - .replace(/\s\s+/g, ' '); + describe('multi select', () => { + it('should support multiselect and selecting a row should enable delete button', done => { + findSelectAllCheckbox().click(); + selectAllCheckboxes(); + + expect(findSelectAllCheckbox().checked).toBe(true); + + Vue.nextTick(() => { + expect(findDeleteBtn().disabled).toBe(false); + done(); + }); + }); + + it('selecting all checkbox should select all rows and enable delete button', done => { + selectAllCheckboxes(); + + Vue.nextTick(() => { + const checkedValues = findAllRowCheckboxes().filter(x => x.checked); + + expect(checkedValues.length).toBe(repoPropsData.list.length); + done(); + }); + }); + + it('deselecting select all checkbox should deselect all rows and disable delete button', done => { + selectAllCheckboxes(); + deselectAllCheckboxes(); + + Vue.nextTick(() => { + const checkedValues = findAllRowCheckboxes().filter(x => x.checked); + + expect(checkedValues.length).toBe(0); + done(); + }); + }); + + it('should delete multiple items when multiple items are selected', done => { + selectAllCheckboxes(); + + Vue.nextTick(() => { + expect(vm.itemsToBeDeleted).toEqual([0, 1]); + expect(findDeleteBtn().disabled).toBe(false); + + findDeleteBtn().click(); + spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve()); + + Vue.nextTick(() => { + const modal = confirmationModal(); + confirmationModal('.btn-danger').click(); + + expect(modal).toExist(); - expect(textRendered).toContain(repoPropsData.list[0].tag); - expect(textRendered).toContain(repoPropsData.list[0].shortRevision); - expect(textRendered).toContain(repoPropsData.list[0].layers); - expect(textRendered).toContain(repoPropsData.list[0].size); + Vue.nextTick(() => { + expect(vm.itemsToBeDeleted).toEqual([]); + expect(vm.multiDeleteItems).toHaveBeenCalledWith({ + path: bulkDeletePath, + items: [firstImage.tag, secondImage.tag], + }); + done(); + }); + }); + }); + }); }); - it('should be possible to delete a registry', () => { - expect(vm.$el.querySelector('.table tbody tr .js-delete-registry')).toBeDefined(); + describe('delete registry', () => { + beforeEach(() => { + vm.itemsToBeDeleted = [0]; + }); + + it('should be possible to delete a registry', done => { + Vue.nextTick(() => { + expect(vm.itemsToBeDeleted).toEqual([0]); + expect(findDeleteBtn()).toBeDefined(); + expect(findDeleteBtn().disabled).toBe(false); + expect(findDeleteBtnRow()).toBeDefined(); + done(); + }); + }); + + it('should call deleteItems and reset itemsToBeDeleted when confirming deletion', done => { + Vue.nextTick(() => { + expect(vm.itemsToBeDeleted).toEqual([0]); + expect(findDeleteBtn().disabled).toBe(false); + findDeleteBtn().click(); + spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve()); + + Vue.nextTick(() => { + confirmationModal('.btn-danger').click(); + + expect(vm.itemsToBeDeleted).toEqual([]); + expect(vm.multiDeleteItems).toHaveBeenCalledWith({ + path: bulkDeletePath, + items: [firstImage.tag], + }); + done(); + }); + }); + }); }); describe('pagination', () => { @@ -46,4 +163,27 @@ describe('table registry', () => { expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); }); }); + + describe('modal content', () => { + it('should show the singular title and image name when deleting a single image', done => { + findDeleteBtnRow().click(); + + Vue.nextTick(() => { + expect(vm.modalTitle).toBe('Remove image'); + expect(vm.modalDescription).toContain(firstImage.tag); + done(); + }); + }); + + it('should show the plural title and image count when deleting more than one image', done => { + selectAllCheckboxes(); + vm.setModalDescription(); + + Vue.nextTick(() => { + expect(vm.modalTitle).toBe('Remove images'); + expect(vm.modalDescription).toContain('<b>2</b> images'); + done(); + }); + }); + }); }); diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js index 22db203e77f..130ab298e89 100644 --- a/spec/javascripts/registry/mock_data.js +++ b/spec/javascripts/registry/mock_data.js @@ -108,6 +108,17 @@ export const repoPropsData = { destroyPath: 'path', canDelete: true, }, + { + tag: 'test-image', + revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4', + shortRevision: 'b969de599', + size: 19, + layers: 10, + location: 'location-2', + createdAt: 1505828744434, + destroyPath: 'path-2', + canDelete: true, + }, ], location: 'location', name: 'foo', diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js index c9aa82dba90..0613ec8e0f1 100644 --- a/spec/javascripts/registry/stores/actions_spec.js +++ b/spec/javascripts/registry/stores/actions_spec.js @@ -105,4 +105,28 @@ describe('Actions Registry Store', () => { ); }); }); + + describe('deleteItem', () => { + it('should perform DELETE request on destroyPath', done => { + const destroyPath = `${TEST_HOST}/mygroup/myproject/container_registry/1.json`; + let deleted = false; + mock.onDelete(destroyPath).replyOnce(() => { + deleted = true; + return [200]; + }); + testAction( + actions.deleteItem, + { + destroyPath, + }, + mockedState, + ) + .then(() => { + expect(mock.history.delete.length).toBe(1); + expect(deleted).toBe(true); + done(); + }) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/releases/components/release_block_spec.js b/spec/javascripts/releases/components/release_block_spec.js index 36b181f24ef..f761a18e326 100644 --- a/spec/javascripts/releases/components/release_block_spec.js +++ b/spec/javascripts/releases/components/release_block_spec.js @@ -14,7 +14,7 @@ describe('Release block', () => { description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>', author_name: 'Release bot', author_email: 'release-bot@example.com', - created_at: '2012-05-28T05:00:00-07:00', + released_at: '2012-05-28T05:00:00-07:00', author: { avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png', id: 482476, @@ -78,8 +78,10 @@ describe('Release block', () => { }; let vm; + const factory = props => mountComponent(Component, { release: props }); + beforeEach(() => { - vm = mountComponent(Component, { release }); + vm = factory(release); }); afterEach(() => { @@ -99,7 +101,7 @@ describe('Release block', () => { }); it('renders release date', () => { - expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.created_at)); + expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.released_at)); }); it('renders number of assets provided', () => { @@ -149,4 +151,14 @@ describe('Release block', () => { ); }); }); + + describe('with upcoming_release flag', () => { + beforeEach(() => { + vm = factory(Object.assign({}, release, { upcoming_release: true })); + }); + + it('renders upcoming release badge', () => { + expect(vm.$el.textContent).toContain('Upcoming Release'); + }); + }); }); diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js index a17494966a3..1f1e626ed33 100644 --- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js +++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js @@ -34,7 +34,7 @@ describe('Grouped Test Reports App', () => { it('renders success summary text', done => { setTimeout(() => { - expect(vm.$el.querySelector('.fa-spinner')).toBeNull(); + expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained no changed test results out of 11 total tests', ); @@ -61,7 +61,7 @@ describe('Grouped Test Reports App', () => { it('renders success summary text', done => { setTimeout(() => { - expect(vm.$el.querySelector('.spinner')).not.toBeNull(); + expect(vm.$el.querySelector('.gl-spinner')).not.toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary results are being parsed', ); @@ -81,7 +81,7 @@ describe('Grouped Test Reports App', () => { it('renders failed summary text + new badge', done => { setTimeout(() => { - expect(vm.$el.querySelector('.spinner')).toBeNull(); + expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 failed test results out of 11 total tests', ); @@ -109,7 +109,7 @@ describe('Grouped Test Reports App', () => { it('renders summary text', done => { setTimeout(() => { - expect(vm.$el.querySelector('.spinner')).toBeNull(); + expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 failed test results and 2 fixed test results out of 11 total tests', ); @@ -137,7 +137,7 @@ describe('Grouped Test Reports App', () => { it('renders summary text', done => { setTimeout(() => { - expect(vm.$el.querySelector('.spinner')).toBeNull(); + expect(vm.$el.querySelector('.gl-spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 fixed test results out of 11 total tests', ); @@ -190,7 +190,7 @@ describe('Grouped Test Reports App', () => { }); it('renders loading summary text with loading icon', done => { - expect(vm.$el.querySelector('.spinner')).not.toBeNull(); + expect(vm.$el.querySelector('.gl-spinner')).not.toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary results are being parsed', ); diff --git a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js index 4c3dd713589..2e1863cff86 100644 --- a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js +++ b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js @@ -13,6 +13,7 @@ describe('Issuable Time Tracker', () => { timeSpent, timeEstimateHumanReadable, timeSpentHumanReadable, + limitToHours, }) => { setFixtures(` <div> @@ -25,6 +26,7 @@ describe('Issuable Time Tracker', () => { timeSpent, humanTimeEstimate: timeEstimateHumanReadable, humanTimeSpent: timeSpentHumanReadable, + limitToHours: Boolean(limitToHours), rootPath: '/', }; @@ -128,6 +130,29 @@ describe('Issuable Time Tracker', () => { }); }); + describe('Comparison pane when limitToHours is true', () => { + beforeEach(() => { + initTimeTrackingComponent({ + timeEstimate: 100000, // 1d 3h + timeSpent: 5000, // 1h 23m + timeEstimateHumanReadable: '', + timeSpentHumanReadable: '', + limitToHours: true, + }); + }); + + it('should show the correct tooltip text', done => { + Vue.nextTick(() => { + expect(vm.showComparisonState).toBe(true); + const $title = vm.$el.querySelector('.time-tracking-content .compare-meter').dataset + .originalTitle; + + expect($title).toBe('Time remaining: 26h 23m'); + done(); + }); + }); + }); + describe('Estimate only pane', () => { beforeEach(() => { initTimeTrackingComponent({ diff --git a/spec/javascripts/sidebar/todo_spec.js b/spec/javascripts/sidebar/todo_spec.js index f46ea5a0499..e7abd19c865 100644 --- a/spec/javascripts/sidebar/todo_spec.js +++ b/spec/javascripts/sidebar/todo_spec.js @@ -53,14 +53,14 @@ describe('SidebarTodo', () => { describe('buttonLabel', () => { it('returns todo button text for marking todo as done when `isTodo` prop is `true`', () => { - expect(vm.buttonLabel).toBe('Mark todo as done'); + expect(vm.buttonLabel).toBe('Mark as done'); }); it('returns todo button text for add todo when `isTodo` prop is `false`', done => { vm.isTodo = false; Vue.nextTick() .then(() => { - expect(vm.buttonLabel).toBe('Add todo'); + expect(vm.buttonLabel).toBe('Add a To Do'); }) .then(done) .catch(done.fail); @@ -131,14 +131,14 @@ describe('SidebarTodo', () => { }); it('check button label computed property', () => { - expect(vm.buttonLabel).toEqual('Mark todo as done'); + expect(vm.buttonLabel).toEqual('Mark as done'); }); it('renders button label element when `collapsed` prop is `false`', () => { const buttonLabelEl = vm.$el.querySelector('span.issuable-todo-inner'); expect(buttonLabelEl).not.toBeNull(); - expect(buttonLabelEl.innerText.trim()).toBe('Mark todo as done'); + expect(buttonLabelEl.innerText.trim()).toBe('Mark as done'); }); it('renders button icon when `collapsed` prop is `true`', done => { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 8c80a425581..ce453d7c483 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -3,19 +3,23 @@ */ import $ from 'jquery'; +import 'core-js/features/set-immediate'; import 'vendor/jasmine-jquery'; import '~/commons'; import Vue from 'vue'; import VueResource from 'vue-resource'; import Translate from '~/vue_shared/translate'; -import CheckEE from '~/vue_shared/mixins/is_ee'; import jasmineDiff from 'jasmine-diff'; +import { config as testUtilsConfig } from '@vue/test-utils'; import { getDefaultAdapter } from '~/lib/utils/axios_utils'; import { FIXTURES_PATH, TEST_HOST } from './test_constants'; import customMatchers from './matchers'; +// Tech debt issue TBD +testUtilsConfig.logModifiedComponents = false; + const isHeadlessChrome = /\bHeadlessChrome\//.test(navigator.userAgent); Vue.config.devtools = !isHeadlessChrome; Vue.config.productionTip = false; @@ -44,7 +48,6 @@ Vue.config.errorHandler = function(err) { Vue.use(VueResource); Vue.use(Translate); -Vue.use(CheckEE); // enable test fixtures jasmine.getFixtures().fixturesPath = FIXTURES_PATH; diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js index 77c206585fe..c97d47a6406 100644 --- a/spec/javascripts/test_constants.js +++ b/spec/javascripts/test_constants.js @@ -1,6 +1,4 @@ -export const FIXTURES_PATH = `/base/${ - process.env.IS_GITLAB_EE ? 'ee/' : '' -}spec/javascripts/fixtures`; +export const FIXTURES_PATH = `/fixtures`; export const TEST_HOST = 'http://test.host'; export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index a2308b0dfdb..fe831094ecf 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -103,7 +103,7 @@ describe('MRWidgetPipeline', () => { it('should render pipeline ID', () => { expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual( - `#${mockData.pipeline.id} (#${mockData.pipeline.iid})`, + `#${mockData.pipeline.id}`, ); }); @@ -150,7 +150,7 @@ describe('MRWidgetPipeline', () => { it('should render pipeline ID', () => { expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual( - `#${mockData.pipeline.id} (#${mockData.pipeline.iid})`, + `#${mockData.pipeline.id}`, ); }); @@ -222,9 +222,7 @@ describe('MRWidgetPipeline', () => { sourceBranchLink: mockCopy.source_branch_link, }); - const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${ - pipeline.details.status.label - } for ${pipeline.commit.short_id} on ${mockCopy.source_branch_link}`; + const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on ${mockCopy.source_branch_link}`; const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText); @@ -247,11 +245,7 @@ describe('MRWidgetPipeline', () => { sourceBranchLink: mockCopy.source_branch_link, }); - const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${ - pipeline.details.status.label - } for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${ - pipeline.merge_request.source_branch - } into ${pipeline.merge_request.target_branch}`; + const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch} into ${pipeline.merge_request.target_branch}`; const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText); @@ -274,11 +268,7 @@ describe('MRWidgetPipeline', () => { sourceBranchLink: mockCopy.source_branch_link, }); - const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${ - pipeline.details.status.label - } for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${ - pipeline.merge_request.source_branch - }`; + const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch}`; const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js index f622f52a7b9..5aac37d28df 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js @@ -18,7 +18,7 @@ describe('MR widget status icon component', () => { it('renders loading icon', () => { vm = mountComponent(Component, { status: 'loading' }); - expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('spinner'); + expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner'); }); }); 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 d93badf8cd3..55a11a72551 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 @@ -38,7 +38,9 @@ describe('MRWidgetAutoMergeFailed', () => { Vue.nextTick(() => { expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); - expect(vm.$el.querySelector('button .loading-container span').classList).toContain('spinner'); + expect(vm.$el.querySelector('button .loading-container span').classList).toContain( + 'gl-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 96e512d222a..70c70eca746 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 @@ -20,7 +20,7 @@ describe('MRWidgetChecking', () => { }); it('renders loading icon', () => { - expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('spinner'); + expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner'); }); it('renders information about merging', () => { 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 bb76616be56..53e1f077610 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 @@ -58,9 +58,11 @@ const createComponent = (customConfig = {}) => { describe('ReadyToMerge', () => { let vm; + let updateMrCountSpy; beforeEach(() => { vm = createComponent(); + updateMrCountSpy = spyOnDependency(ReadyToMerge, 'refreshUserMergeRequestCounts'); }); afterEach(() => { @@ -234,24 +236,26 @@ describe('ReadyToMerge', () => { }); }); - describe('shouldShowMergeOptionsDropdown', () => { - it('should return false when no auto merge strategies are available', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + describe('shouldShowMergeImmediatelyDropdown', () => { + it('should return false if no pipeline is active', () => { + Vue.set(vm.mr, 'isPipelineActive', false); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); - expect(vm.shouldShowMergeOptionsDropdown).toBe(false); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); - it('should return true when at least one auto merge strategy is available', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]); + it('should return false if "Pipelines must succeed" is enabled for the current project', () => { + Vue.set(vm.mr, 'isPipelineActive', true); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); - expect(vm.shouldShowMergeOptionsDropdown).toBe(true); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); - it('should return false when pipeline active but only merge when pipeline succeeds set in project options', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); + it('should return true if the MR\'s pipeline is active and "Pipelines must succeed" is not enabled for the current project', () => { + Vue.set(vm.mr, 'isPipelineActive', true); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); - expect(vm.shouldShowMergeOptionsDropdown).toBe(false); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(true); }); }); @@ -461,6 +465,7 @@ describe('ReadyToMerge', () => { expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); expect(eventHub.$emit).toHaveBeenCalledWith('FetchActionsContent'); expect(vm.initiateRemoveSourceBranchPolling).toHaveBeenCalled(); + expect(updateMrCountSpy).toHaveBeenCalled(); expect(cpc).toBeFalsy(); expect(spc).toBeTruthy(); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js index d6d8eecfcb9..cb656525f06 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js @@ -21,7 +21,7 @@ describe('Squash before merge component', () => { }); describe('checkbox', () => { - const findCheckbox = () => wrapper.find('.qa-squash-checkbox'); + const findCheckbox = () => wrapper.find('.js-squash-checkbox'); it('is unchecked if passed value prop is false', () => { createComponent({ diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js index bd64d7b2926..5bd1af56bcc 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js @@ -10,7 +10,7 @@ describe('UnresolvedDiscussions', () => { vm.$destroy(); }); - describe('with discussions path', () => { + describe('with threads path', () => { beforeEach(() => { vm = mountComponent(Component, { mr: { @@ -21,7 +21,7 @@ describe('UnresolvedDiscussions', () => { it('should have correct elements', () => { expect(vm.$el.innerText).toContain( - 'There are unresolved discussions. Please resolve these discussions', + 'There are unresolved threads. Please resolve these threads', ); expect(vm.$el.innerText).toContain('Create an issue to resolve them later'); @@ -29,14 +29,14 @@ describe('UnresolvedDiscussions', () => { }); }); - describe('without discussions path', () => { + describe('without threads path', () => { beforeEach(() => { vm = mountComponent(Component, { mr: {} }); }); it('should not show create issue link if user cannot create issue', () => { expect(vm.$el.innerText).toContain( - 'There are unresolved discussions. Please resolve these discussions', + 'There are unresolved threads. Please resolve these threads', ); expect(vm.$el.querySelector('.js-create-issue')).toEqual(null); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 3c9a5cece90..a55d5537df7 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -61,7 +61,6 @@ export default { "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", pipeline: { id: 172, - iid: 32, user: { name: 'Administrator', username: 'root', @@ -219,7 +218,8 @@ export default { '/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1', email_patches_path: '/root/acets-app/merge_requests/22.patch', plain_diff_path: '/root/acets-app/merge_requests/22.diff', - status_path: '/root/acets-app/merge_requests/22.json', + merge_request_basic_path: '/root/acets-app/merge_requests/22.json?serializer=basic', + merge_request_widget_path: '/root/acets-app/merge_requests/22/widget.json', merge_check_path: '/root/acets-app/merge_requests/22/merge_check', ci_environments_status_url: '/root/acets-app/merge_requests/22/ci_environments_status', project_archived: false, @@ -233,6 +233,8 @@ export default { 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', troubleshooting_docs_path: 'help', merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md', + merge_train_when_pipeline_succeeds_docs_path: + '/help/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/#startadd-to-merge-train-when-pipeline-succeeds', squash: true, visual_review_app_available: true, merge_trains_enabled: true, @@ -243,8 +245,6 @@ export default { export const mockStore = { pipeline: { id: 0, - iid: 0, - path: '/root/acets-app/pipelines/0', details: { status: { details_path: '/root/review-app-tester/pipelines/66', @@ -262,8 +262,6 @@ export const mockStore = { }, mergePipeline: { id: 1, - iid: 1, - path: '/root/acets-app/pipelines/0', details: { status: { details_path: '/root/review-app-tester/pipelines/66', diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 08f7a17515e..30e0504e4e1 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -473,7 +473,7 @@ describe('mrWidgetOptions', () => { vm.mr.relatedLinks = { assignToMe: null, closing: ` - <a class="close-related-link" href="#'> + <a class="close-related-link" href="#"> Close </a> `, @@ -544,7 +544,6 @@ describe('mrWidgetOptions', () => { ]; const deploymentMockData = { id: 15, - iid: 7, name: 'review/diplo', url: '/root/acets-review-apps/environments/15', stop_url: '/root/acets-review-apps/environments/15/stop', @@ -591,7 +590,6 @@ describe('mrWidgetOptions', () => { vm.mr.state = 'merged'; vm.mr.mergePipeline = { id: 127, - iid: 35, user: { id: 1, name: 'Administrator', diff --git a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js deleted file mode 100644 index 634ba8403d5..00000000000 --- a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import Vue from 'vue'; -import changedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; -import createComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('Changed file icon', () => { - let vm; - - function factory(props = {}) { - const component = Vue.extend(changedFileIcon); - - vm = createComponent(component, { - ...props, - file: { - tempFile: false, - changed: true, - }, - }); - } - - afterEach(() => { - vm.$destroy(); - }); - - it('centers icon', () => { - factory({ - isCentered: true, - }); - - expect(vm.$el.classList).toContain('ml-auto'); - }); - - describe('changedIcon', () => { - it('equals file-modified when not a temp file and has changes', () => { - factory(); - - expect(vm.changedIcon).toBe('file-modified'); - }); - - it('equals file-addition when a temp file', () => { - factory(); - - vm.file.tempFile = true; - - expect(vm.changedIcon).toBe('file-addition'); - }); - }); - - describe('changedIconClass', () => { - it('includes file-modified when not a temp file', () => { - factory(); - - expect(vm.changedIconClass).toContain('file-modified'); - }); - - it('includes file-addition when a temp file', () => { - factory(); - - vm.file.tempFile = true; - - expect(vm.changedIconClass).toContain('file-addition'); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js index 5bea8c43da3..1f61e19fa84 100644 --- a/spec/javascripts/vue_shared/components/file_icon_spec.js +++ b/spec/javascripts/vue_shared/components/file_icon_spec.js @@ -72,7 +72,7 @@ describe('File Icon component', () => { const { classList } = vm.$el.querySelector('.loading-container span'); - expect(classList.contains('spinner')).toEqual(true); + expect(classList.contains('gl-spinner')).toEqual(true); }); it('should add a special class and a size class', () => { diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js index 7da69e3fa84..6abcac5c0ff 100644 --- a/spec/javascripts/vue_shared/components/file_row_spec.js +++ b/spec/javascripts/vue_shared/components/file_row_spec.js @@ -90,6 +90,19 @@ describe('File row component', () => { expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null); }); + it('is not rendered for `moved` entries in subfolders', () => { + createComponent({ + file: { + path: 't5', + moved: true, + tree: [], + }, + level: 2, + }); + + expect(vm.$el.nodeType).not.toEqual(1); + }); + describe('new dropdown', () => { beforeEach(() => { createComponent({ 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 a9c1a67b39b..2b059e5e9f4 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -88,7 +88,7 @@ describe('Header CI Component', () => { vm.actions[0].isLoading = true; Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn .spinner').getAttribute('style')).toBeFalsy(); + expect(vm.$el.querySelector('.btn .gl-spinner').getAttribute('style')).toBeFalsy(); done(); }); }); diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js deleted file mode 100644 index d4be2451f0b..00000000000 --- a/spec/javascripts/vue_shared/components/markdown/header_spec.js +++ /dev/null @@ -1,111 +0,0 @@ -import Vue from 'vue'; -import $ from 'jquery'; -import headerComponent from '~/vue_shared/components/markdown/header.vue'; - -describe('Markdown field header component', () => { - let vm; - - beforeEach(done => { - const Component = Vue.extend(headerComponent); - - vm = new Component({ - propsData: { - previewMarkdown: false, - }, - }).$mount(); - - Vue.nextTick(done); - }); - - it('renders markdown header buttons', () => { - const buttons = [ - 'Add bold text', - 'Add italic text', - 'Insert a quote', - 'Insert code', - 'Add a link', - 'Add a bullet list', - 'Add a numbered list', - 'Add a task list', - 'Add a table', - 'Insert suggestion', - 'Go full screen', - ]; - const elements = vm.$el.querySelectorAll('.toolbar-btn'); - - elements.forEach((buttonEl, index) => { - expect(buttonEl.getAttribute('data-original-title')).toBe(buttons[index]); - }); - }); - - it('renders `write` link as active when previewMarkdown is false', () => { - expect(vm.$el.querySelector('li:nth-child(1)').classList.contains('active')).toBeTruthy(); - }); - - it('renders `preview` link as active when previewMarkdown is true', done => { - vm.previewMarkdown = true; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('li:nth-child(2)').classList.contains('active')).toBeTruthy(); - - done(); - }); - }); - - it('emits toggle markdown event when clicking preview', () => { - spyOn(vm, '$emit'); - - vm.$el.querySelector('.js-preview-link').click(); - - expect(vm.$emit).toHaveBeenCalledWith('preview-markdown'); - - vm.$el.querySelector('.js-write-link').click(); - - expect(vm.$emit).toHaveBeenCalledWith('write-markdown'); - }); - - it('does not emit toggle markdown event when triggered from another form', () => { - spyOn(vm, '$emit'); - - $(document).triggerHandler('markdown-preview:show', [ - $( - '<form><div class="js-vue-markdown-field"><textarea class="markdown-area"></textarea></div></form>', - ), - ]); - - expect(vm.$emit).not.toHaveBeenCalled(); - }); - - it('blurs preview link after click', done => { - const link = vm.$el.querySelector('li:nth-child(2) button'); - spyOn(HTMLElement.prototype, 'blur'); - - link.click(); - - setTimeout(() => { - expect(link.blur).toHaveBeenCalled(); - - done(); - }); - }); - - it('renders markdown table template', () => { - expect(vm.mdTable).toEqual( - '| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |', - ); - }); - - it('renders suggestion template', () => { - vm.lineContent = 'Some content'; - - expect(vm.mdSuggestion).toEqual('```suggestion:-0+0\n{text}\n```'); - }); - - it('does not render suggestion button if `canSuggest` is set to false', () => { - vm.canSuggest = false; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.qa-suggestion-btn')).toBe(null); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js index ea74cb9eb21..dc929e83eb7 100644 --- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js @@ -60,7 +60,7 @@ describe('Suggestion Diff component', () => { describe('init', () => { it('renders a suggestion header', () => { - expect(vm.$el.querySelector('.qa-suggestion-diff-header')).not.toBeNull(); + expect(vm.$el.querySelector('.js-suggestion-diff-header')).not.toBeNull(); }); it('renders a diff table with syntax highlighting', () => { diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index 42abb4d83f0..258530f32f7 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -217,7 +217,7 @@ describe('Pagination component', () => { change: spy, }); - expect(component.$el.querySelector('.js-next-button').textContent.trim()).toEqual('Next'); + expect(component.$el.querySelector('.js-next-button').textContent.trim()).toEqual('Next ›'); component.$el.querySelector('.js-next-button .page-link').click(); @@ -237,7 +237,7 @@ describe('Pagination component', () => { change: spy, }); - expect(component.$el.querySelector('.js-next-button').textContent.trim()).toEqual('Next'); + expect(component.$el.querySelector('.js-next-button').textContent.trim()).toEqual('Next ›'); component.$el.querySelector('.js-next-button .page-link').click(); diff --git a/spec/javascripts/vue_shared/components/tooltip_on_truncate_spec.js b/spec/javascripts/vue_shared/components/tooltip_on_truncate_spec.js index 997d84dcc42..ad8d5a53291 100644 --- a/spec/javascripts/vue_shared/components/tooltip_on_truncate_spec.js +++ b/spec/javascripts/vue_shared/components/tooltip_on_truncate_spec.js @@ -1,68 +1,72 @@ -import { mountComponentWithRender } from 'spec/helpers/vue_mount_component_helper'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; const TEST_TITLE = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do'; -const CLASS_SHOW_TOOLTIP = 'js-show-tooltip'; -const STYLE_TRUNCATED = { - display: 'inline-block', - 'max-width': '20px', -}; -const STYLE_NORMAL = { - display: 'inline-block', - 'max-width': '1000px', -}; - -function mountTooltipOnTruncate(options, createChildren) { - return mountComponentWithRender(h => h(TooltipOnTruncate, options, createChildren(h)), '#app'); -} +const STYLE_TRUNCATED = 'display: inline-block; max-width: 20px;'; +const STYLE_NORMAL = 'display: inline-block; max-width: 1000px;'; -describe('TooltipOnTruncate component', () => { - let vm; +const localVue = createLocalVue(); - beforeEach(() => { - const el = document.createElement('div'); - el.id = 'app'; - document.body.appendChild(el); - }); +const createElementWithStyle = (style, content) => `<a href="#" style="${style}">${content}</a>`; + +describe('TooltipOnTruncate component', () => { + let wrapper; + + const createComponent = ({ propsData, ...options } = {}) => { + wrapper = shallowMount(localVue.extend(TooltipOnTruncate), { + localVue, + sync: false, + attachToDocument: true, + propsData: { + title: TEST_TITLE, + ...propsData, + }, + ...options, + }); + }; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); + const hasTooltip = () => wrapper.classes('js-show-tooltip'); + describe('with default target', () => { it('renders tooltip if truncated', done => { - const options = { - style: STYLE_TRUNCATED, - props: { - title: TEST_TITLE, + createComponent({ + attrs: { + style: STYLE_TRUNCATED, }, - }; - - vm = mountTooltipOnTruncate(options, () => [TEST_TITLE]); + slots: { + default: [TEST_TITLE], + }, + }); - vm.$nextTick() + wrapper.vm + .$nextTick() .then(() => { - expect(vm.$el).toHaveClass(CLASS_SHOW_TOOLTIP); - expect(vm.$el).toHaveData('original-title', TEST_TITLE); - expect(vm.$el).toHaveData('placement', 'top'); + expect(hasTooltip()).toBe(true); + expect(wrapper.attributes('data-original-title')).toEqual(TEST_TITLE); + expect(wrapper.attributes('data-placement')).toEqual('top'); }) .then(done) .catch(done.fail); }); it('does not render tooltip if normal', done => { - const options = { - style: STYLE_NORMAL, - props: { - title: TEST_TITLE, + createComponent({ + attrs: { + style: STYLE_NORMAL, }, - }; - - vm = mountTooltipOnTruncate(options, () => [TEST_TITLE]); + slots: { + default: [TEST_TITLE], + }, + }); - vm.$nextTick() + wrapper.vm + .$nextTick() .then(() => { - expect(vm.$el).not.toHaveClass(CLASS_SHOW_TOOLTIP); + expect(hasTooltip()).toBe(false); }) .then(done) .catch(done.fail); @@ -71,37 +75,41 @@ describe('TooltipOnTruncate component', () => { describe('with child target', () => { it('renders tooltip if truncated', done => { - const options = { - style: STYLE_NORMAL, - props: { - title: TEST_TITLE, + createComponent({ + attrs: { + style: STYLE_NORMAL, + }, + propsData: { truncateTarget: 'child', }, - }; - - vm = mountTooltipOnTruncate(options, h => [h('a', { style: STYLE_TRUNCATED }, TEST_TITLE)]); + slots: { + default: createElementWithStyle(STYLE_TRUNCATED, TEST_TITLE), + }, + }); - vm.$nextTick() + wrapper.vm + .$nextTick() .then(() => { - expect(vm.$el).toHaveClass(CLASS_SHOW_TOOLTIP); + expect(hasTooltip()).toBe(true); }) .then(done) .catch(done.fail); }); it('does not render tooltip if normal', done => { - const options = { - props: { - title: TEST_TITLE, + createComponent({ + propsData: { truncateTarget: 'child', }, - }; - - vm = mountTooltipOnTruncate(options, h => [h('a', { style: STYLE_NORMAL }, TEST_TITLE)]); + slots: { + default: createElementWithStyle(STYLE_NORMAL, TEST_TITLE), + }, + }); - vm.$nextTick() + wrapper.vm + .$nextTick() .then(() => { - expect(vm.$el).not.toHaveClass(CLASS_SHOW_TOOLTIP); + expect(hasTooltip()).toBe(false); }) .then(done) .catch(done.fail); @@ -110,22 +118,25 @@ describe('TooltipOnTruncate component', () => { describe('with fn target', () => { it('renders tooltip if truncated', done => { - const options = { - style: STYLE_NORMAL, - props: { - title: TEST_TITLE, + createComponent({ + attrs: { + style: STYLE_NORMAL, + }, + propsData: { truncateTarget: el => el.childNodes[1], }, - }; - - vm = mountTooltipOnTruncate(options, h => [ - h('a', { style: STYLE_NORMAL }, TEST_TITLE), - h('span', { style: STYLE_TRUNCATED }, TEST_TITLE), - ]); + slots: { + default: [ + createElementWithStyle('', TEST_TITLE), + createElementWithStyle(STYLE_TRUNCATED, TEST_TITLE), + ], + }, + }); - vm.$nextTick() + wrapper.vm + .$nextTick() .then(() => { - expect(vm.$el).toHaveClass(CLASS_SHOW_TOOLTIP); + expect(hasTooltip()).toBe(true); }) .then(done) .catch(done.fail); @@ -134,20 +145,25 @@ describe('TooltipOnTruncate component', () => { describe('placement', () => { it('sets data-placement when tooltip is rendered', done => { - const options = { - props: { - title: TEST_TITLE, - truncateTarget: 'child', - placement: 'bottom', - }, - }; + const placement = 'bottom'; - vm = mountTooltipOnTruncate(options, h => [h('a', { style: STYLE_TRUNCATED }, TEST_TITLE)]); + createComponent({ + propsData: { + placement, + }, + attrs: { + style: STYLE_TRUNCATED, + }, + slots: { + default: TEST_TITLE, + }, + }); - vm.$nextTick() + wrapper.vm + .$nextTick() .then(() => { - expect(vm.$el).toHaveClass(CLASS_SHOW_TOOLTIP); - expect(vm.$el).toHaveData('placement', options.props.placement); + expect(hasTooltip()).toBe(true); + expect(wrapper.attributes('data-placement')).toEqual(placement); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js b/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js new file mode 100644 index 00000000000..f1ca5f61496 --- /dev/null +++ b/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js @@ -0,0 +1,38 @@ +import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; + +/** + * We're testing this directive's hooks as pure functions + * since behaviour of this directive is highly-dependent + * on underlying DOM methods. + */ +describe('AutofocusOnShow directive', () => { + describe('with input invisible on component render', () => { + let el; + + beforeAll(() => { + setFixtures('<div id="container" style="display: none;"><input id="inputel"/></div>'); + el = document.querySelector('#inputel'); + }); + + it('should bind IntersectionObserver on input element', () => { + spyOn(el, 'focus'); + + autofocusonshow.inserted(el); + + expect(el.visibilityObserver).toBeDefined(); + expect(el.focus).not.toHaveBeenCalled(); + }); + + it('should stop IntersectionObserver on input element on unbind hook', () => { + el.visibilityObserver = { + disconnect: () => {}, + }; + spyOn(el.visibilityObserver, 'disconnect'); + + autofocusonshow.unbind(el); + + expect(el.visibilityObserver).toBeDefined(); + expect(el.visibilityObserver.disconnect).toHaveBeenCalled(); + }); + }); +}); |
