diff options
Diffstat (limited to 'spec/javascripts')
35 files changed, 456 insertions, 274 deletions
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 9e5b0bd3efe..0e656858182 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -9,7 +9,6 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; -import '~/lib/utils/url_utility'; import '~/boards/models/issue'; import '~/boards/models/label'; import '~/boards/models/list'; diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 10b88878c2a..41dcb19df3c 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -4,7 +4,6 @@ /* global mockBoardService */ import Vue from 'vue'; -import '~/lib/utils/url_utility'; import '~/boards/models/issue'; import '~/boards/models/label'; import '~/boards/models/list'; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index d4627223a12..eead396ca7e 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -9,7 +9,6 @@ import Vue from 'vue'; -import '~/lib/utils/url_utility'; import '~/boards/models/issue'; import '~/boards/models/label'; import '~/boards/models/list'; diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 230c15e5de6..5111632d681 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -1,8 +1,8 @@ +import * as urlUtils from '~/lib/utils/url_utility'; import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; -import '~/lib/utils/url_utility'; import '~/lib/utils/common_utils'; import '~/filtered_search/filtered_search_token_keys'; import '~/filtered_search/filtered_search_tokenizer'; @@ -162,7 +162,7 @@ describe('Filtered Search Manager', () => { it('should search with a single word', (done) => { input.value = 'searchTerm'; - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + spyOn(urlUtils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=searchTerm`); done(); }); @@ -173,7 +173,7 @@ describe('Filtered Search Manager', () => { it('should search with multiple words', (done) => { input.value = 'awesome search terms'; - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + spyOn(urlUtils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); done(); }); @@ -184,7 +184,7 @@ describe('Filtered Search Manager', () => { it('should search with special characters', (done) => { input.value = '~!@#$%^&*()_+{}:<>,.?/'; - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + spyOn(urlUtils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); done(); }); @@ -198,7 +198,7 @@ describe('Filtered Search Manager', () => { ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} `); - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + spyOn(urlUtils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&label_name[]=bug`); done(); }); diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index 4f20e31f511..a3fa07d5bc2 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -253,7 +253,7 @@ describe('Fly out sidebar navigation', () => { it('shows collapsed only sub-items if icon only sidebar', () => { const subItems = el.querySelector('.sidebar-sub-level-items'); const sidebar = document.createElement('div'); - sidebar.classList.add('sidebar-icons-only'); + sidebar.classList.add('sidebar-collapsed-desktop'); subItems.classList.add('is-fly-out-only'); setSidebar(sidebar); @@ -343,7 +343,7 @@ describe('Fly out sidebar navigation', () => { it('returns true when active & collapsed sidebar', () => { const sidebar = document.createElement('div'); - sidebar.classList.add('sidebar-icons-only'); + sidebar.classList.add('sidebar-collapsed-desktop'); el.classList.add('active'); setSidebar(sidebar); diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index ca048123bf7..b13d1bf8dff 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -2,7 +2,7 @@ import '~/gl_dropdown'; import '~/lib/utils/common_utils'; -import '~/lib/utils/url_utility'; +import * as urlUtils from '~/lib/utils/url_utility'; describe('glDropdown', function describeDropdown() { preloadFixtures('static/gl_dropdown.html.raw'); @@ -137,13 +137,13 @@ describe('glDropdown', function describeDropdown() { expect(this.dropdownContainerElement).toHaveClass('open'); const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; navigateWithKeys('down', randomIndex, () => { - spyOn(gl.utils, 'visitUrl').and.stub(); + spyOn(urlUtils, 'visitUrl').and.stub(); navigateWithKeys('enter', null, () => { expect(this.dropdownContainerElement).not.toHaveClass('open'); const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); expect(link).toHaveClass('is-active'); const linkedLocation = link.attr('href'); - if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation); + if (linkedLocation && linkedLocation !== '#') expect(urlUtils.visitUrl).toHaveBeenCalledWith(linkedLocation); }); }); }); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 59d4f7c45c6..97e39f6411b 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -1,9 +1,9 @@ import Vue from 'vue'; +import * as utils from '~/lib/utils/url_utility'; import appComponent from '~/groups/components/app.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import groupItemComponent from '~/groups/components/group_item.vue'; - import eventHub from '~/groups/event_hub'; import GroupsStore from '~/groups/store/groups_store'; import GroupsService from '~/groups/service/groups_service'; @@ -176,7 +176,7 @@ describe('AppComponent', () => { it('should fetch groups for provided page details and update window state', (done) => { spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups)); spyOn(vm, 'updateGroups').and.callThrough(); - spyOn(gl.utils, 'mergeUrlParams').and.callThrough(); + spyOn(utils, 'mergeUrlParams').and.callThrough(); spyOn(window.history, 'replaceState'); spyOn($, 'scrollTo'); @@ -192,7 +192,7 @@ describe('AppComponent', () => { setTimeout(() => { expect(vm.isLoading).toBeFalsy(); expect($.scrollTo).toHaveBeenCalledWith(0); - expect(gl.utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); + expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); expect(window.history.replaceState).toHaveBeenCalledWith({ page: jasmine.any(String), }, jasmine.any(String), jasmine.any(String)); diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js index 0f4fbdae445..618d0022e4f 100644 --- a/spec/javascripts/groups/components/group_item_spec.js +++ b/spec/javascripts/groups/components/group_item_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; - +import * as urlUtils from '~/lib/utils/url_utility'; import groupItemComponent from '~/groups/components/group_item.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import eventHub from '~/groups/event_hub'; @@ -136,13 +136,13 @@ describe('GroupItemComponent', () => { const group = Object.assign({}, mockParentGroupItem); group.childrenCount = 0; const newVm = createComponent(group); - spyOn(gl.utils, 'visitUrl').and.stub(); + spyOn(urlUtils, 'visitUrl').and.stub(); spyOn(eventHub, '$emit'); newVm.onClickRowGroup(event); setTimeout(() => { expect(eventHub.$emit).not.toHaveBeenCalled(); - expect(gl.utils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath); + expect(urlUtils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath); done(); }, 0); }); diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js index fb9c7e59031..ce3add1fd90 100644 --- a/spec/javascripts/image_diff/helpers/badge_helper_spec.js +++ b/spec/javascripts/image_diff/helpers/badge_helper_spec.js @@ -89,15 +89,8 @@ describe('badge helper', () => { }); it('should create icon comment button', () => { - const iconEl = buttonEl.querySelector('i'); + const iconEl = buttonEl.querySelector('svg'); expect(iconEl).toBeDefined(); - expect(iconEl.classList.contains('fa')).toEqual(true); - expect(iconEl.classList.contains('fa-comment-o')).toEqual(true); - }); - - it('should have .image-comment-badge.inverted in button class', () => { - expect(buttonEl.classList.contains('image-comment-badge')).toEqual(true); - expect(buttonEl.classList.contains('inverted')).toEqual(true); }); }); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 53b8a368d28..729c3c29f22 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import '~/render_math'; import '~/render_gfm'; +import * as urlUtils from '~/lib/utils/url_utility'; import issuableApp from '~/issue_show/components/app.vue'; import eventHub from '~/issue_show/event_hub'; import issueShowData from '../mock_data'; @@ -180,7 +181,7 @@ describe('Issuable output', () => { }); it('does not redirect if issue has not moved', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ json() { @@ -196,7 +197,7 @@ describe('Issuable output', () => { setTimeout(() => { expect( - gl.utils.visitUrl, + urlUtils.visitUrl, ).not.toHaveBeenCalled(); done(); @@ -204,7 +205,7 @@ describe('Issuable output', () => { }); it('redirects if returned web_url has changed', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ json() { @@ -220,7 +221,7 @@ describe('Issuable output', () => { setTimeout(() => { expect( - gl.utils.visitUrl, + urlUtils.visitUrl, ).toHaveBeenCalledWith('/testing-issue-move'); done(); @@ -319,7 +320,7 @@ describe('Issuable output', () => { describe('deleteIssuable', () => { it('changes URL when deleted', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { resolve({ json() { @@ -332,7 +333,7 @@ describe('Issuable output', () => { setTimeout(() => { expect( - gl.utils.visitUrl, + urlUtils.visitUrl, ).toHaveBeenCalledWith('/test'); done(); @@ -340,7 +341,7 @@ describe('Issuable output', () => { }); it('stops polling when deleting', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn(vm.poll, 'stop').and.callThrough(); spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { resolve({ diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index 20c4caa865d..4f06237deb5 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -1,6 +1,6 @@ import { bytesToKiB } from '~/lib/utils/number_utils'; +import * as urlUtils from '~/lib/utils/url_utility'; import '~/lib/utils/datetime_utility'; -import '~/lib/utils/url_utility'; import Job from '~/job'; import '~/breakpoints'; @@ -65,7 +65,7 @@ describe('Job', () => { const deferred2 = $.Deferred(); const deferred3 = $.Deferred(); spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); deferred1.resolve({ html: '<span>Update<span>', @@ -103,7 +103,7 @@ describe('Job', () => { spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); deferred1.resolve({ html: '<span>Update<span>', @@ -134,7 +134,7 @@ describe('Job', () => { describe('truncated information', () => { describe('when size is less than total', () => { it('shows information about truncated log', () => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); const deferred = $.Deferred(); spyOn($, 'ajax').and.returnValue(deferred.promise()); @@ -153,7 +153,7 @@ describe('Job', () => { it('shows the size in KiB', () => { const size = 50; - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); const deferred = $.Deferred(); spyOn($, 'ajax').and.returnValue(deferred.promise()); @@ -179,7 +179,7 @@ describe('Job', () => { spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); deferred1.resolve({ html: '<span>Update</span>', @@ -214,7 +214,7 @@ describe('Job', () => { it('renders the raw link', () => { const deferred = $.Deferred(); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn($, 'ajax').and.returnValue(deferred.promise()); deferred.resolve({ @@ -236,7 +236,7 @@ describe('Job', () => { describe('when size is equal than total', () => { it('does not show the trunctated information', () => { const deferred = $.Deferred(); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn($, 'ajax').and.returnValue(deferred.promise()); deferred.resolve({ @@ -257,7 +257,7 @@ describe('Job', () => { describe('output trace', () => { beforeEach(() => { const deferred = $.Deferred(); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn($, 'ajax').and.returnValue(deferred.promise()); deferred.resolve({ diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index e441d1153ed..5076435e7a8 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,6 +1,7 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ /* global Notes */ +import * as urlUtils from '~/lib/utils/url_utility'; import '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; import '~/breakpoints'; @@ -333,7 +334,7 @@ import 'vendor/jquery.scrollTo'; describe('with note fragment hash', () => { it('should expand and scroll to linked fragment hash #note_xxx', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); + spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(noteId.length).toBeGreaterThan(0); @@ -345,7 +346,7 @@ import 'vendor/jquery.scrollTo'; }); it('should gracefully ignore non-existant fragment hash', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); @@ -354,7 +355,7 @@ import 'vendor/jquery.scrollTo'; describe('with line number fragment hash', () => { it('should gracefully ignore line number fragment hash', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId); + spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(noteLineNumId.length).toBeGreaterThan(0); @@ -387,7 +388,7 @@ import 'vendor/jquery.scrollTo'; describe('with note fragment hash', () => { it('should expand and scroll to linked fragment hash #note_xxx', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); + spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); @@ -400,7 +401,7 @@ import 'vendor/jquery.scrollTo'; }); it('should gracefully ignore non-existant fragment hash', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); @@ -409,7 +410,7 @@ import 'vendor/jquery.scrollTo'; describe('with line number fragment hash', () => { it('should gracefully ignore line number fragment hash', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId); + spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(noteLineNumId.length).toBeGreaterThan(0); diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js index dea42d755d4..bf6ada8185e 100644 --- a/spec/javascripts/monitoring/graph/deployment_spec.js +++ b/spec/javascripts/monitoring/graph/deployment_spec.js @@ -118,7 +118,7 @@ describe('MonitoringDeployment', () => { ).not.toEqual('display: none;'); }); - it('shows the refText inside a text element with the deploy-info-text class', () => { + it('contains date, refs and the "deployed" text', () => { reducedDeploymentData[0].showDeploymentFlag = true; const component = createComponent({ showDeployInfo: true, @@ -129,8 +129,31 @@ describe('MonitoringDeployment', () => { }); expect( - component.$el.querySelector('.deploy-info-text').firstChild.nodeValue.trim(), - ).toEqual(component.refText(reducedDeploymentData[0])); + component.$el.querySelectorAll('.deploy-info-text'), + ).toContainText('Deployed'); + + expect( + component.$el.querySelectorAll('.deploy-info-text'), + ).toContainText('Wed, May 31'); + + expect( + component.$el.querySelectorAll('.deploy-info-text'), + ).toContainText(component.refText(reducedDeploymentData[0])); + }); + + it('contains a link to the commit contents', () => { + reducedDeploymentData[0].showDeploymentFlag = true; + const component = createComponent({ + showDeployInfo: true, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphWidth: 440, + graphHeightOffset: 120, + }); + + expect( + component.$el.querySelectorAll('.deploy-info-text-link')[0].parentElement.getAttribute('xlink:href'), + ).not.toEqual(''); }); it('should contain a hidden gradient', () => { diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js index fd79abe241a..b1d69752bad 100644 --- a/spec/javascripts/monitoring/graph_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -4,6 +4,8 @@ import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; import eventHub from '~/monitoring/event_hub'; import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data'; +const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags'; +const projectPath = 'http://test.host/frontend-fixtures/environments-project'; const createComponent = (propsData) => { const Component = Vue.extend(Graph); @@ -25,6 +27,8 @@ describe('Graph', () => { classType: 'col-md-6', updateAspectRatio: false, deploymentData, + tagsPath, + projectPath, }); expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title); @@ -37,6 +41,8 @@ describe('Graph', () => { classType: 'col-md-6', updateAspectRatio: false, deploymentData, + tagsPath, + projectPath, }); const transformedHeight = `${component.graphHeight - 100}`; @@ -50,6 +56,8 @@ describe('Graph', () => { classType: 'col-md-6', updateAspectRatio: false, deploymentData, + tagsPath, + projectPath, }); const viewBoxArray = component.outerViewBox.split(' '); @@ -65,6 +73,8 @@ describe('Graph', () => { classType: 'col-md-6', updateAspectRatio: false, deploymentData, + tagsPath, + projectPath, }); spyOn(eventHub, '$emit'); @@ -81,6 +91,8 @@ describe('Graph', () => { classType: 'col-md-6', updateAspectRatio: false, deploymentData, + tagsPath, + projectPath, }); expect(component.yAxisLabel).toEqual(component.graphData.y_label); @@ -98,6 +110,8 @@ describe('Graph', () => { hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'), currentDeployXPos: null, }, + tagsPath, + projectPath, }); component.positionFlag(); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 6b34855b8b2..1f4e858e731 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -2430,33 +2430,39 @@ export const deploymentData = [ id: 111, iid: 3, sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', ref: { name: 'master' }, created_at: '2017-05-31T21:23:37.881Z', tag: false, + tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false', 'last?': true }, { id: 110, iid: 2, sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', ref: { name: 'master' }, created_at: '2017-05-30T20:08:04.629Z', tag: false, + tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false', 'last?': false }, { id: 109, iid: 1, sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2', + commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2', ref: { name: 'update2-readme' }, created_at: '2017-05-30T17:42:38.409Z', tag: false, + tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false', 'last?': false } ]; diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index 04a7f8e32f1..20e352dd8bd 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import Autosize from 'autosize'; import store from '~/notes/stores'; -import issueCommentForm from '~/notes/components/issue_comment_form.vue'; +import issueCommentForm from '~/notes/components/comment_form.vue'; import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data'; import { keyboardDownEvent } from '../../issue_show/helpers'; diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 8e43037f356..7c8d6685ee1 100644 --- a/spec/javascripts/notes/components/issue_note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -1,26 +1,15 @@ import Vue from 'vue'; -import issueNotesApp from '~/notes/components/issue_notes_app.vue'; +import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; import * as mockData from '../mock_data'; +import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper'; -describe('issue_note_app', () => { +describe('note_app', () => { let mountComponent; let vm; - const individualNoteInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(mockData.individualNoteServerResponse), { - status: 200, - })); - }; - - const discussionNoteInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(mockData.discussionNoteServerResponse), { - status: 200, - })); - }; - beforeEach(() => { - const IssueNotesApp = Vue.extend(issueNotesApp); + const IssueNotesApp = Vue.extend(notesApp); mountComponent = (data) => { const props = data || { @@ -74,16 +63,16 @@ describe('issue_note_app', () => { describe('render', () => { beforeEach(() => { - Vue.http.interceptors.push(individualNoteInterceptor); + Vue.http.interceptors.push(mockData.individualNoteInterceptor); vm = mountComponent(); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor); }); it('should render list of notes', (done) => { - const note = mockData.individualNoteServerResponse[0].notes[0]; + const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET['/gitlab-org/gitlab-ce/issues/26/discussions.json'][0].notes[0]; setTimeout(() => { expect( @@ -129,13 +118,16 @@ describe('issue_note_app', () => { describe('update note', () => { describe('individual note', () => { beforeEach(() => { - Vue.http.interceptors.push(individualNoteInterceptor); - spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); + Vue.http.interceptors.push(mockData.individualNoteInterceptor); + spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + Vue.http.interceptors = _.without( + Vue.http.interceptors, + mockData.individualNoteInterceptor, + ); }); it('renders edit form', (done) => { @@ -149,28 +141,36 @@ describe('issue_note_app', () => { }); it('calls the service to update the note', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { + getSetTimeoutPromise() + .then(() => { + vm.$el.querySelector('.js-note-edit').click(); + }) + .then(Vue.nextTick) + .then(() => { vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; vm.$el.querySelector('.js-vue-issue-save').click(); expect(service.updateNote).toHaveBeenCalled(); - done(); - }); - }, 0); + }) + // Wait for the requests to finish before destroying + .then(Vue.nextTick) + .then(done) + .catch(done.fail); }); }); describe('dicussion note', () => { beforeEach(() => { - Vue.http.interceptors.push(discussionNoteInterceptor); - spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); + Vue.http.interceptors.push(mockData.discussionNoteInterceptor); + spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, discussionNoteInterceptor); + Vue.http.interceptors = _.without( + Vue.http.interceptors, + mockData.discussionNoteInterceptor, + ); }); it('renders edit form', (done) => { @@ -184,16 +184,21 @@ describe('issue_note_app', () => { }); it('updates the note and resets the edit form', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { + getSetTimeoutPromise() + .then(() => { + vm.$el.querySelector('.js-note-edit').click(); + }) + .then(Vue.nextTick) + .then(() => { vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; vm.$el.querySelector('.js-vue-issue-save').click(); expect(service.updateNote).toHaveBeenCalled(); - done(); - }); - }, 0); + }) + // Wait for the requests to finish before destroying + .then(Vue.nextTick) + .then(done) + .catch(done.fail); }); }); }); @@ -216,12 +221,12 @@ describe('issue_note_app', () => { describe('edit form', () => { beforeEach(() => { - Vue.http.interceptors.push(individualNoteInterceptor); + Vue.http.interceptors.push(mockData.individualNoteInterceptor); vm = mountComponent(); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor); }); it('should render markdown docs url', (done) => { diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js index 37aad50737b..b42e7943b98 100644 --- a/spec/javascripts/notes/components/issue_note_body_spec.js +++ b/spec/javascripts/notes/components/note_body_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import noteBody from '~/notes/components/issue_note_body.vue'; +import noteBody from '~/notes/components/note_body.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note_body component', () => { diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index d42ef239711..86e9e2a32a9 100644 --- a/spec/javascripts/notes/components/issue_note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import issueNoteForm from '~/notes/components/issue_note_form.vue'; +import issueNoteForm from '~/notes/components/note_form.vue'; import { noteableDataMock, notesDataMock } from '../mock_data'; import { keyboardDownEvent } from '../../issue_show/helpers'; diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index b6ae55d44f5..19504e4f7c8 100644 --- a/spec/javascripts/notes/components/issue_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import issueDiscussion from '~/notes/components/issue_discussion.vue'; +import issueDiscussion from '~/notes/components/noteable_discussion.vue'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; describe('issue_discussion component', () => { @@ -30,7 +30,7 @@ describe('issue_discussion component', () => { it('should render discussion header', () => { expect(vm.$el.querySelector('.discussion-header')).toBeDefined(); - expect(vm.$el.querySelectorAll('.notes li').length).toEqual(discussionMock.notes.length); + expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length); }); describe('actions', () => { diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index 73fd188dbe5..c8a6cb7e612 100644 --- a/spec/javascripts/notes/components/issue_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import issueNote from '~/notes/components/issue_note.vue'; +import issueNote from '~/notes/components/noteable_note.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note', () => { @@ -41,4 +41,19 @@ describe('issue_note', () => { it('should render issue body', () => { expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); }); + + it('prevents note preview xss', (done) => { + const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`; + const alertSpy = spyOn(window, 'alert'); + vm.updateNote = () => new Promise($.noop); + + vm.formUpdateHandler(noteBody, null, $.noop); + + setTimeout(() => { + expect(alertSpy).not.toHaveBeenCalled(); + expect(vm.note.note_html).toEqual(_.escape(noteBody)); + done(); + }, 0); + }); }); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 42497de3c55..6b608adff15 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -312,138 +312,212 @@ export const loggedOutnoteableData = { "preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue" } -export const individualNoteServerResponse = [{ - "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "expanded": true, - "notes": [{ - "id": 1390, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-01T17:09:33.762Z", - "updated_at": "2017-08-01T17:09:33.762Z", - "system": false, - "noteable_id": 98, - "noteable_type": "Issue", - "type": null, - "human_access": "Owner", - "note": "sdfdsaf", - "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", - "current_user": { - "can_edit": true +export const INDIVIDUAL_NOTE_RESPONSE_MAP = { + 'GET': { + '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{ + "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "expanded": true, + "notes": [{ + "id": 1390, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-01T17:09:33.762Z", + "updated_at": "2017-08-01T17:09:33.762Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "sdfdsaf", + "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "emoji_awardable": true, + "award_emoji": [{ + "name": "baseball", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }, { + "name": "art", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1390" + }], + "individual_note": true + }, { + "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "expanded": true, + "notes": [{ + "id": 1391, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-02T10:51:38.685Z", + "updated_at": "2017-08-02T10:51:38.685Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "New note!", + "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1391" + }], + "individual_note": true + }], + '/gitlab-org/gitlab-ce/noteable/issue/98/notes': { + last_fetched_at: 1512900838, + notes: [], }, - "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "emoji_awardable": true, - "award_emoji": [{ - "name": "baseball", - "user": { + }, + 'PUT': { + '/gitlab-org/gitlab-ce/notes/1471': { + "commands_changes": null, + "valid": true, + "id": 1471, + "attachment": null, + "author": { "id": 1, "name": "Root", - "username": "root" - } - }, { - "name": "art", - "user": { + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-08T16:53:00.666Z", + "updated_at": "2017-12-10T11:03:21.876Z", + "system": false, + "noteable_id": 124, + "noteable_type": "Issue", + "noteable_iid": 29, + "type": "DiscussionNote", + "human_access": "Owner", + "note": "Adding a comment", + "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", + "last_edited_at": "2017-12-10T11:03:21.876Z", + "last_edited_by": { "id": 1, - "name": "Root", - "username": "root" - } - }], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1390" - }], - "individual_note": true - }, { - "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "expanded": true, - "notes": [{ - "id": 1391, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-02T10:51:38.685Z", - "updated_at": "2017-08-02T10:51:38.685Z", - "system": false, - "noteable_id": 98, - "noteable_type": "Issue", - "type": null, - "human_access": "Owner", - "note": "New note!", - "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", - "current_user": { - "can_edit": true + "name": 'Root', + "username": 'root', + "state": 'active', + "avatar_url": null, + "path": '/root', + }, + "current_user": { + "can_edit": true + }, + "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1471" }, - "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "emoji_awardable": true, - "award_emoji": [], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1391" - }], - "individual_note": true -}]; + } +}; -export const discussionNoteServerResponse = [{ - "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "expanded": true, - "notes": [{ - "id": 1471, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-08T16:53:00.666Z", - "updated_at": "2017-08-08T16:53:00.666Z", - "system": false, - "noteable_id": 124, - "noteable_type": "Issue", - "noteable_iid": 29, - "type": "DiscussionNote", - "human_access": "Owner", - "note": "Adding a comment", - "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", - "current_user": { - "can_edit": true - }, - "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "emoji_awardable": true, - "award_emoji": [], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1471" - }], - "individual_note": false -}]; +export const DISCUSSION_NOTE_RESPONSE_MAP = { + ...INDIVIDUAL_NOTE_RESPONSE_MAP, + 'GET': { + ...INDIVIDUAL_NOTE_RESPONSE_MAP.GET, + '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{ + "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "expanded": true, + "notes": [{ + "id": 1471, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-08T16:53:00.666Z", + "updated_at": "2017-08-08T16:53:00.666Z", + "system": false, + "noteable_id": 124, + "noteable_type": "Issue", + "noteable_iid": 29, + "type": "DiscussionNote", + "human_access": "Owner", + "note": "Adding a comment", + "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1471" + }], + "individual_note": false + }], + }, +}; + +export function individualNoteInterceptor(request, next) { + const body = INDIVIDUAL_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url]; + + next(request.respondWith(JSON.stringify(body), { + status: 200, + })); +} + +export function discussionNoteInterceptor(request, next) { + const body = DISCUSSION_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url]; + + next(request.respondWith(JSON.stringify(body), { + status: 200, + })); +} diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 677a389b88f..2612c5fd7bc 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ /* global Notes */ +import * as urlUtils from '~/lib/utils/url_utility'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; @@ -168,8 +169,7 @@ import '~/notes'; }); it('sets target when hash matches', () => { - spyOn(gl.utils, 'getLocationHash'); - gl.utils.getLocationHash.and.returnValue(hash); + spyOn(urlUtils, 'getLocationHash').and.returnValue(hash); Notes.updateNoteTargetSelector($note); @@ -178,8 +178,7 @@ import '~/notes'; }); it('unsets target when hash does not match', () => { - spyOn(gl.utils, 'getLocationHash'); - gl.utils.getLocationHash.and.returnValue('note_doesnotexist'); + spyOn(urlUtils, 'getLocationHash').and.returnValue('note_doesnotexist'); Notes.updateNoteTargetSelector($note); @@ -187,8 +186,7 @@ import '~/notes'; }); it('unsets target when there is not a hash fragment anymore', () => { - spyOn(gl.utils, 'getLocationHash'); - gl.utils.getLocationHash.and.returnValue(null); + spyOn(urlUtils, 'getLocationHash').and.returnValue(null); Notes.updateNoteTargetSelector($note); diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js index 1d3e1263371..fe3ea996eac 100644 --- a/spec/javascripts/pager_spec.js +++ b/spec/javascripts/pager_spec.js @@ -1,5 +1,6 @@ /* global fixture */ +import * as utils from '~/lib/utils/url_utility'; import '~/pager'; describe('pager', () => { @@ -30,7 +31,7 @@ describe('pager', () => { it('should use current url if data-href attribute not provided', () => { const href = `${gl.TEST_HOST}/some_list`; - spyOn(gl.utils, 'removeParams').and.returnValue(href); + spyOn(utils, 'removeParams').and.returnValue(href); Pager.init(); expect(Pager.url).toBe(href); }); @@ -44,9 +45,9 @@ describe('pager', () => { it('keeps extra query parameters from url', () => { window.history.replaceState({}, null, '?filter=test&offset=100'); const href = `${gl.TEST_HOST}/some_list?filter=test`; - spyOn(gl.utils, 'removeParams').and.returnValue(href); + spyOn(utils, 'removeParams').and.returnValue(href); Pager.init(); - expect(gl.utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']); + expect(utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']); expect(Pager.url).toEqual(href); }); }); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 23c87610d83..35e36e9c353 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -113,4 +113,35 @@ describe('pipeline graph job component', () => { component.$el.querySelector('a').classList.contains('css-class-job-name'), ).toBe(true); }); + + describe('status label', () => { + it('should not render status label when it is not provided', () => { + component = mountComponent(JobComponent, { + job: { + id: 4256, + name: 'test', + status: { + icon: 'icon_status_success', + }, + }, + }); + + expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test'); + }); + + it('should not render status label when it is provided', () => { + component = mountComponent(JobComponent, { + job: { + id: 4256, + name: 'test', + status: { + icon: 'icon_status_success', + label: 'success', + }, + }, + }); + + expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test - success'); + }); + }); }); diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index 1c794123095..72712e058e5 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import store from '~/repo/stores'; import service from '~/repo/services'; import repoCommitSection from '~/repo/components/repo_commit_section.vue'; @@ -97,7 +98,7 @@ describe('RepoCommitSection', () => { }); it('redirects to MR creation page if start new MR checkbox checked', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); vm.startNewMR = true; vm.makeCommit(); @@ -105,7 +106,7 @@ describe('RepoCommitSection', () => { getSetTimeoutPromise() .then(() => Vue.nextTick()) .then(() => { - expect(gl.utils.visitUrl).toHaveBeenCalled(); + expect(urlUtils.visitUrl).toHaveBeenCalled(); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js index 393a797c6a3..2bbc49d5a9f 100644 --- a/spec/javascripts/repo/stores/actions/tree_spec.js +++ b/spec/javascripts/repo/stores/actions/tree_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import store from '~/repo/stores'; import service from '~/repo/services'; import { file, resetStore } from '../../helpers'; @@ -255,7 +256,7 @@ describe('Multi-file store tree actions', () => { let row; beforeEach(() => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); row = { url: 'submoduleurl', @@ -276,7 +277,7 @@ describe('Multi-file store tree actions', () => { it('opens submodule URL', (done) => { store.dispatch('clickedTreeRow', row) .then(() => { - expect(gl.utils.visitUrl).toHaveBeenCalledWith('submoduleurl'); + expect(urlUtils.visitUrl).toHaveBeenCalledWith('submoduleurl'); done(); }).catch(done.fail); diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js index f2a7a698912..21d87e46216 100644 --- a/spec/javascripts/repo/stores/actions_spec.js +++ b/spec/javascripts/repo/stores/actions_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import store from '~/repo/stores'; import service from '~/repo/services'; import { resetStore, file } from '../helpers'; @@ -10,11 +11,11 @@ describe('Multi-file store actions', () => { describe('redirectToUrl', () => { it('calls visitUrl', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); store.dispatch('redirectToUrl', 'test') .then(() => { - expect(gl.utils.visitUrl).toHaveBeenCalledWith('test'); + expect(urlUtils.visitUrl).toHaveBeenCalledWith('test'); done(); }) @@ -326,13 +327,13 @@ describe('Multi-file store actions', () => { }); it('redirects to new merge request page', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch='; store.dispatch('commitChanges', { payload, newMr: true }) .then(() => { - expect(gl.utils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master'); + expect(urlUtils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master'); done(); }).catch(done.fail); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index fdfc59a6f12..1e4c2c9faad 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -3,6 +3,7 @@ import '~/gl_dropdown'; import '~/search_autocomplete'; import '~/lib/utils/common_utils'; +import * as urlUtils from '~/lib/utils/url_utility'; (function() { var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; @@ -121,7 +122,7 @@ import '~/lib/utils/common_utils'; loadFixtures('static/search_autocomplete.html.raw'); // Prevent turbolinks from triggering within gl_dropdown - spyOn(window.gl.utils, 'visitUrl').and.returnValue(true); + spyOn(urlUtils, 'visitUrl').and.returnValue(true); window.gon = {}; window.gon.current_user_id = userId; diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js index 929ba75e67d..b97e24d9dcf 100644 --- a/spec/javascripts/sidebar/sidebar_assignees_spec.js +++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js @@ -4,20 +4,29 @@ import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarStore from '~/sidebar/stores/sidebar_store'; import Mock from './mock_data'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('sidebar assignees', () => { - let component; - let SidebarAssigneeComponent; + let vm; + let mediator; + let sidebarAssigneesEl; preloadFixtures('issues/open-issue.html.raw'); beforeEach(() => { Vue.http.interceptors.push(Mock.sidebarMockInterceptor); - SidebarAssigneeComponent = Vue.extend(SidebarAssignees); - spyOn(SidebarMediator.prototype, 'saveAssignees').and.callThrough(); - spyOn(SidebarMediator.prototype, 'assignYourself').and.callThrough(); - this.mediator = new SidebarMediator(Mock.mediator); + loadFixtures('issues/open-issue.html.raw'); - this.sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees'); + + mediator = new SidebarMediator(Mock.mediator); + spyOn(mediator, 'saveAssignees').and.callThrough(); + spyOn(mediator, 'assignYourself').and.callThrough(); + + const SidebarAssigneeComponent = Vue.extend(SidebarAssignees); + sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees'); + vm = mountComponent(SidebarAssigneeComponent, { + mediator, + field: sidebarAssigneesEl.dataset.field, + }, sidebarAssigneesEl); }); afterEach(() => { @@ -28,30 +37,24 @@ describe('sidebar assignees', () => { }); it('calls the mediator when saves the assignees', () => { - component = new SidebarAssigneeComponent() - .$mount(this.sidebarAssigneesEl); - component.saveAssignees(); - - expect(SidebarMediator.prototype.saveAssignees).toHaveBeenCalled(); + vm.saveAssignees(); + expect(mediator.saveAssignees).toHaveBeenCalled(); }); it('calls the mediator when "assignSelf" method is called', () => { - component = new SidebarAssigneeComponent() - .$mount(this.sidebarAssigneesEl); - component.assignSelf(); + vm.assignSelf(); - expect(SidebarMediator.prototype.assignYourself).toHaveBeenCalled(); - expect(this.mediator.store.assignees.length).toEqual(1); + expect(mediator.assignYourself).toHaveBeenCalled(); + expect(mediator.store.assignees.length).toEqual(1); }); it('hides assignees until fetched', (done) => { - component = new SidebarAssigneeComponent().$mount(this.sidebarAssigneesEl); - const currentAssignee = this.sidebarAssigneesEl.querySelector('.value'); + const currentAssignee = sidebarAssigneesEl.querySelector('.value'); expect(currentAssignee).toBe(null); - component.store.isFetching.assignees = false; + vm.store.isFetching.assignees = false; Vue.nextTick(() => { - expect(component.$el.querySelector('.value')).toBeVisible(); + expect(vm.$el.querySelector('.value')).toBeVisible(); done(); }); }); diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index 14c34d5a78c..9efd109b996 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; import SidebarService from '~/sidebar/services/sidebar_service'; @@ -85,12 +86,12 @@ describe('Sidebar mediator', () => { const moveToProjectId = 7; this.mediator.store.setMoveToProjectId(moveToProjectId); spyOn(this.mediator.service, 'moveIssue').and.callThrough(); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); this.mediator.moveIssue() .then(() => { expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId); - expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5'); + expect(urlUtils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5'); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js index 7adf22b0f1f..a6113cb0bae 100644 --- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js +++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js @@ -26,11 +26,14 @@ describe('Sidebar Subscriptions', function () { }); it('calls the mediator toggleSubscription on event', () => { - spyOn(SidebarMediator.prototype, 'toggleSubscription').and.returnValue(Promise.resolve()); - vm = mountComponent(SidebarSubscriptions, {}); + const mediator = new SidebarMediator(); + spyOn(mediator, 'toggleSubscription').and.returnValue(Promise.resolve()); + vm = mountComponent(SidebarSubscriptions, { + mediator, + }); eventHub.$emit('toggleSubscription'); - expect(SidebarMediator.prototype.toggleSubscription).toHaveBeenCalled(); + expect(mediator.toggleSubscription).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index fd7aa332d17..6897c991066 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -17,6 +17,12 @@ Vue.config.warnHandler = (msg, vm, trace) => { fail(`${msg}${trace}`); }; +let hasVueErrors = false; +Vue.config.errorHandler = function (err) { + hasVueErrors = true; + fail(err); +}; + Vue.use(VueResource); // enable test fixtures @@ -72,7 +78,7 @@ testsContext.keys().forEach(function (path) { describe('test errors', () => { beforeAll((done) => { - if (hasUnhandledPromiseRejections || hasVueWarnings) { + if (hasUnhandledPromiseRejections || hasVueWarnings || hasVueErrors) { setTimeout(done, 1000); } else { done(); @@ -86,6 +92,10 @@ describe('test errors', () => { it('has no Vue warnings', () => { expect(hasVueWarnings).toBe(false); }); + + it('has no Vue error', () => { + expect(hasVueErrors).toBe(false); + }); }); // if we're generating coverage reports, make sure to include all files so diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index 7d3c9319a11..59e16f0786e 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -1,3 +1,4 @@ +import * as urlUtils from '~/lib/utils/url_utility'; import Todos from '~/todos'; import '~/lib/utils/common_utils'; @@ -16,7 +17,7 @@ describe('Todos', () => { it('opens the todo url', (done) => { const todoLink = todoItem.dataset.url; - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + spyOn(urlUtils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(todoLink); done(); }); @@ -31,7 +32,7 @@ describe('Todos', () => { beforeEach(() => { metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); - visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {}); + visitUrlSpy = spyOn(urlUtils, 'visitUrl').and.callFake(() => {}); windowOpenSpy = spyOn(window, 'open').and.callFake(() => {}); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js index 7ee998c8fce..d5b8947c86f 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; @@ -108,13 +109,13 @@ describe('MRWidgetDeployment', () => { it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => { spyOn(window, 'confirm').and.returnValue(true); spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true)); - spyOn(gl.utils, 'visitUrl').and.returnValue(true); + spyOn(urlUtils, 'visitUrl').and.returnValue(true); vm = mockStopEnvironment(); expect(window.confirm).toHaveBeenCalled(); expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url); setTimeout(() => { - expect(gl.utils.visitUrl).toHaveBeenCalledWith(url); + expect(urlUtils.visitUrl).toHaveBeenCalledWith(url); done(); }, 333); }); |