diff options
Diffstat (limited to 'spec/javascripts')
88 files changed, 4258 insertions, 1414 deletions
diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/abuse_reports_spec.js index 13cab81dd60..7f6b5873011 100644 --- a/spec/javascripts/abuse_reports_spec.js +++ b/spec/javascripts/abuse_reports_spec.js @@ -1,43 +1,41 @@ import '~/lib/utils/text_utility'; -import '~/abuse_reports'; - -((global) => { - describe('Abuse Reports', () => { - const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; - const MAX_MESSAGE_LENGTH = 500; - - let $messages; - - const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); - const findMessage = searchText => $messages.filter( - (index, element) => element.innerText.indexOf(searchText) > -1, - ).first(); - - preloadFixtures(FIXTURE); - - beforeEach(function () { - loadFixtures(FIXTURE); - this.abuseReports = new global.AbuseReports(); - $messages = $('.abuse-reports .message'); - }); - - it('should truncate long messages', () => { - const $longMessage = findMessage('LONG MESSAGE'); - expect($longMessage.data('original-message')).toEqual(jasmine.anything()); - assertMaxLength($longMessage); - }); - - it('should not truncate short messages', () => { - const $shortMessage = findMessage('SHORT MESSAGE'); - expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything()); - }); - - it('should allow clicking a truncated message to expand and collapse the full message', () => { - const $longMessage = findMessage('LONG MESSAGE'); - $longMessage.click(); - expect($longMessage.data('original-message').length).toEqual($longMessage.text().length); - $longMessage.click(); - assertMaxLength($longMessage); - }); +import AbuseReports from '~/abuse_reports'; + +describe('Abuse Reports', () => { + const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; + const MAX_MESSAGE_LENGTH = 500; + + let $messages; + + const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); + const findMessage = searchText => $messages.filter( + (index, element) => element.innerText.indexOf(searchText) > -1, + ).first(); + + preloadFixtures(FIXTURE); + + beforeEach(function () { + loadFixtures(FIXTURE); + this.abuseReports = new AbuseReports(); + $messages = $('.abuse-reports .message'); + }); + + it('should truncate long messages', () => { + const $longMessage = findMessage('LONG MESSAGE'); + expect($longMessage.data('original-message')).toEqual(jasmine.anything()); + assertMaxLength($longMessage); + }); + + it('should not truncate short messages', () => { + const $shortMessage = findMessage('SHORT MESSAGE'); + expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything()); + }); + + it('should allow clicking a truncated message to expand and collapse the full message', () => { + const $longMessage = findMessage('LONG MESSAGE'); + $longMessage.click(); + expect($longMessage.data('original-message').length).toEqual($longMessage.text().length); + $longMessage.click(); + assertMaxLength($longMessage); }); -})(window.gl); +}); diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js index 46e072a8ebb..c93b7cc6cac 100644 --- a/spec/javascripts/ajax_loading_spinner_spec.js +++ b/spec/javascripts/ajax_loading_spinner_spec.js @@ -1,6 +1,6 @@ import 'jquery'; import 'jquery-ujs'; -import '~/ajax_loading_spinner'; +import AjaxLoadingSpinner from '~/ajax_loading_spinner'; describe('Ajax Loading Spinner', () => { const fixtureTemplate = 'static/ajax_loading_spinner.html.raw'; @@ -8,7 +8,7 @@ describe('Ajax Loading Spinner', () => { beforeEach(() => { loadFixtures(fixtureTemplate); - gl.AjaxLoadingSpinner.init(); + AjaxLoadingSpinner.init(); }); it('change current icon with spinner icon and disable link while waiting ajax response', (done) => { diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js index 11f2a950678..c3e67550f05 100644 --- a/spec/javascripts/blob/notebook/index_spec.js +++ b/spec/javascripts/blob/notebook/index_spec.js @@ -117,7 +117,7 @@ describe('iPython notebook renderer', () => { it('shows error message', () => { expect( document.querySelector('.md').textContent.trim(), - ).toBe('An error occured whilst parsing the file.'); + ).toBe('An error occurred whilst parsing the file.'); }); }); @@ -153,7 +153,7 @@ describe('iPython notebook renderer', () => { it('shows error message', () => { expect( document.querySelector('.md').textContent.trim(), - ).toBe('An error occured whilst loading the file. Please try again later.'); + ).toBe('An error occurred whilst loading the file. Please try again later.'); }); }); }); diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js index bbeaf95e68d..51bf3086627 100644 --- a/spec/javascripts/blob/pdf/index_spec.js +++ b/spec/javascripts/blob/pdf/index_spec.js @@ -76,7 +76,7 @@ describe('PDF renderer', () => { it('shows error message', () => { expect( document.querySelector('.md').textContent.trim(), - ).toBe('An error occured whilst loading the file. Please try again later.'); + ).toBe('An error occurred whilst loading the file. Please try again later.'); }); }); }); diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 35149611095..d5b0f23e7b7 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -289,4 +289,18 @@ describe('Build', () => { }); }); }); + + describe('getBuildTrace', () => { + it('should request build trace with state parameter', (done) => { + spyOn(jQuery, 'ajax').and.callThrough(); + new Build(); + + setTimeout(() => { + expect(jQuery.ajax).toHaveBeenCalledWith( + { url: `${BUILD_URL}/trace.json`, data: { state: '' } }, + ); + done(); + }, 0); + }); + }); }); diff --git a/spec/javascripts/clusters_spec.js b/spec/javascripts/clusters_spec.js new file mode 100644 index 00000000000..eb1cd6eb804 --- /dev/null +++ b/spec/javascripts/clusters_spec.js @@ -0,0 +1,79 @@ +import Clusters from '~/clusters'; + +describe('Clusters', () => { + let cluster; + preloadFixtures('clusters/show_cluster.html.raw'); + + beforeEach(() => { + loadFixtures('clusters/show_cluster.html.raw'); + cluster = new Clusters(); + }); + + describe('toggle', () => { + it('should update the button and the input field on click', () => { + cluster.toggleButton.click(); + + expect( + cluster.toggleButton.classList, + ).not.toContain('checked'); + + expect( + cluster.toggleInput.getAttribute('value'), + ).toEqual('false'); + }); + }); + + describe('updateContainer', () => { + describe('when creating cluster', () => { + it('should show the creating container', () => { + cluster.updateContainer('creating'); + + expect( + cluster.creatingContainer.classList.contains('hidden'), + ).toBeFalsy(); + expect( + cluster.successContainer.classList.contains('hidden'), + ).toBeTruthy(); + expect( + cluster.errorContainer.classList.contains('hidden'), + ).toBeTruthy(); + }); + }); + + describe('when cluster is created', () => { + it('should show the success container', () => { + cluster.updateContainer('created'); + + expect( + cluster.creatingContainer.classList.contains('hidden'), + ).toBeTruthy(); + expect( + cluster.successContainer.classList.contains('hidden'), + ).toBeFalsy(); + expect( + cluster.errorContainer.classList.contains('hidden'), + ).toBeTruthy(); + }); + }); + + describe('when cluster has error', () => { + it('should show the error container', () => { + cluster.updateContainer('errored', 'this is an error'); + + expect( + cluster.creatingContainer.classList.contains('hidden'), + ).toBeTruthy(); + expect( + cluster.successContainer.classList.contains('hidden'), + ).toBeTruthy(); + expect( + cluster.errorContainer.classList.contains('hidden'), + ).toBeFalsy(); + + expect( + cluster.errorReasonContainer.textContent, + ).toContain('this is an error'); + }); + }); + }); +}); diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 454f187ccbc..9fc047b1f5e 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -29,6 +29,8 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + errorStateSvgPath: 'foo', autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -65,6 +67,8 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + errorStateSvgPath: 'foo', autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -117,6 +121,8 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + errorStateSvgPath: 'foo', autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -139,6 +145,8 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + errorStateSvgPath: 'foo', autoDevopsHelpPath: 'foo', }, }).$mount(); diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index ace95000468..e5a5e3293b9 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -1,77 +1,73 @@ -/* global CommitsList */ - import 'vendor/jquery.endless-scroll'; import '~/pager'; -import '~/commits'; - -(() => { - describe('Commits List', () => { - beforeEach(() => { - setFixtures(` - <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master"> - <input id="commits-search"> - </form> - <ol id="commits-list"></ol> - `); - }); +import CommitsList from '~/commits'; - it('should be defined', () => { - expect(CommitsList).toBeDefined(); - }); +describe('Commits List', () => { + beforeEach(() => { + setFixtures(` + <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master"> + <input id="commits-search"> + </form> + <ol id="commits-list"></ol> + `); + }); - describe('processCommits', () => { - it('should join commit headers', () => { - CommitsList.$contentList = $(` - <div> - <li class="commit-header" data-day="2016-09-20"> - <span class="day">20 Sep, 2016</span> - <span class="commits-count">1 commit</span> - </li> - <li class="commit"></li> - </div> - `); + it('should be defined', () => { + expect(CommitsList).toBeDefined(); + }); - const data = ` + describe('processCommits', () => { + it('should join commit headers', () => { + CommitsList.$contentList = $(` + <div> <li class="commit-header" data-day="2016-09-20"> <span class="day">20 Sep, 2016</span> <span class="commits-count">1 commit</span> </li> <li class="commit"></li> - `; + </div> + `); - // The last commit header should be removed - // since the previous one has the same data-day value. - expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0); - }); + const data = ` + <li class="commit-header" data-day="2016-09-20"> + <span class="day">20 Sep, 2016</span> + <span class="commits-count">1 commit</span> + </li> + <li class="commit"></li> + `; + + // The last commit header should be removed + // since the previous one has the same data-day value. + expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0); }); + }); - describe('on entering input', () => { - let ajaxSpy; + describe('on entering input', () => { + let ajaxSpy; - beforeEach(() => { - CommitsList.init(25); - CommitsList.searchField.val(''); + beforeEach(() => { + CommitsList.init(25); + CommitsList.searchField.val(''); - spyOn(history, 'replaceState').and.stub(); - ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => { - req.success({ - data: '<li>Result</li>', - }); + spyOn(history, 'replaceState').and.stub(); + ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => { + req.success({ + data: '<li>Result</li>', }); }); + }); - it('should save the last search string', () => { - CommitsList.searchField.val('GitLab'); - CommitsList.filterResults(); - expect(ajaxSpy).toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual('GitLab'); - }); + it('should save the last search string', () => { + CommitsList.searchField.val('GitLab'); + CommitsList.filterResults(); + expect(ajaxSpy).toHaveBeenCalled(); + expect(CommitsList.lastSearch).toEqual('GitLab'); + }); - it('should not make ajax call if the input does not change', () => { - CommitsList.filterResults(); - expect(ajaxSpy).not.toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual(''); - }); + it('should not make ajax call if the input does not change', () => { + CommitsList.filterResults(); + expect(ajaxSpy).not.toHaveBeenCalled(); + expect(CommitsList.lastSearch).toEqual(''); }); }); -})(); +}); diff --git a/spec/javascripts/cycle_analytics/banner_spec.js b/spec/javascripts/cycle_analytics/banner_spec.js new file mode 100644 index 00000000000..fb6b7fee168 --- /dev/null +++ b/spec/javascripts/cycle_analytics/banner_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import banner from '~/cycle_analytics/components/banner.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Cycle analytics banner', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(banner); + vm = mountComponent(Component, { + documentationLink: 'path', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render cycle analytics information', () => { + expect( + vm.$el.querySelector('h4').textContent.trim(), + ).toEqual('Introducing Cycle Analytics'); + expect( + vm.$el.querySelector('p').textContent.trim(), + ).toContain('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.'); + expect( + vm.$el.querySelector('a').textContent.trim(), + ).toEqual('Read more'); + expect( + vm.$el.querySelector('a').getAttribute('href'), + ).toEqual('path'); + }); + + it('should emit an event when close button is clicked', () => { + spyOn(vm, '$emit'); + + vm.$el.querySelector('.js-ca-dismiss-button').click(); + + expect(vm.$emit).toHaveBeenCalled(); + }); +}); diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js index 2fb9eb0ca85..13e9fe00a00 100644 --- a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js +++ b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; -import limitWarningComp from '~/cycle_analytics/components/limit_warning_component'; +import limitWarningComp from '~/cycle_analytics/components/limit_warning_component.vue'; Vue.use(Translate); diff --git a/spec/javascripts/cycle_analytics/total_time_component_spec.js b/spec/javascripts/cycle_analytics/total_time_component_spec.js new file mode 100644 index 00000000000..31b65fd1cde --- /dev/null +++ b/spec/javascripts/cycle_analytics/total_time_component_spec.js @@ -0,0 +1,58 @@ +import Vue from 'vue'; +import component from '~/cycle_analytics/components/total_time_component.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Total time component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('With data', () => { + it('should render information for days and hours', () => { + vm = mountComponent(Component, { + time: { + days: 3, + hours: 4, + }, + }); + + expect(vm.$el.textContent.trim()).toEqual('3 days 4 hrs'); + }); + + it('should render information for hours and minutes', () => { + vm = mountComponent(Component, { + time: { + hours: 4, + mins: 35, + }, + }); + + expect(vm.$el.textContent.trim()).toEqual('4 hrs 35 mins'); + }); + + it('should render information for seconds', () => { + vm = mountComponent(Component, { + time: { + seconds: 45, + }, + }); + + expect(vm.$el.textContent.trim()).toEqual('45 s'); + }); + }); + + describe('Without data', () => { + it('should render no information', () => { + vm = mountComponent(Component); + + expect(vm.$el.textContent.trim()).toEqual('--'); + }); + }); +}); diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index fdaea5c0b0c..7e62d356bd2 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -14,6 +14,10 @@ describe('Environments Folder View', () => { window.history.pushState({}, null, 'environments/folders/build'); }); + afterEach(() => { + window.history.pushState({}, null, '/'); + }); + let component; describe('successfull request', () => { diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js deleted file mode 100644 index 114d282e48a..00000000000 --- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js +++ /dev/null @@ -1,219 +0,0 @@ -import Cookies from 'js-cookie'; -import { - getCookieName, - getSelector, - showPopover, - hidePopover, - dismiss, - mouseleave, - mouseenter, - setupDismissButton, -} from '~/feature_highlight/feature_highlight_helper'; - -describe('feature highlight helper', () => { - describe('getCookieName', () => { - it('returns `feature-highlighted-` prefix', () => { - const cookieId = 'cookieId'; - expect(getCookieName(cookieId)).toEqual(`feature-highlighted-${cookieId}`); - }); - }); - - describe('getSelector', () => { - it('returns js-feature-highlight selector', () => { - const highlightId = 'highlightId'; - expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`); - }); - }); - - describe('showPopover', () => { - it('returns true when popover is shown', () => { - const context = { - hasClass: () => false, - popover: () => {}, - addClass: () => {}, - }; - - expect(showPopover.call(context)).toEqual(true); - }); - - it('returns false when popover is already shown', () => { - const context = { - hasClass: () => true, - }; - - expect(showPopover.call(context)).toEqual(false); - }); - - it('shows popover', (done) => { - const context = { - hasClass: () => false, - popover: () => {}, - addClass: () => {}, - }; - - spyOn(context, 'popover').and.callFake((method) => { - expect(method).toEqual('show'); - done(); - }); - - showPopover.call(context); - }); - - it('adds disable-animation and js-popover-show class', (done) => { - const context = { - hasClass: () => false, - popover: () => {}, - addClass: () => {}, - }; - - spyOn(context, 'addClass').and.callFake((classNames) => { - expect(classNames).toEqual('disable-animation js-popover-show'); - done(); - }); - - showPopover.call(context); - }); - }); - - describe('hidePopover', () => { - it('returns true when popover is hidden', () => { - const context = { - hasClass: () => true, - popover: () => {}, - removeClass: () => {}, - }; - - expect(hidePopover.call(context)).toEqual(true); - }); - - it('returns false when popover is already hidden', () => { - const context = { - hasClass: () => false, - }; - - expect(hidePopover.call(context)).toEqual(false); - }); - - it('hides popover', (done) => { - const context = { - hasClass: () => true, - popover: () => {}, - removeClass: () => {}, - }; - - spyOn(context, 'popover').and.callFake((method) => { - expect(method).toEqual('hide'); - done(); - }); - - hidePopover.call(context); - }); - - it('removes disable-animation and js-popover-show class', (done) => { - const context = { - hasClass: () => true, - popover: () => {}, - removeClass: () => {}, - }; - - spyOn(context, 'removeClass').and.callFake((classNames) => { - expect(classNames).toEqual('disable-animation js-popover-show'); - done(); - }); - - hidePopover.call(context); - }); - }); - - describe('dismiss', () => { - const context = { - hide: () => {}, - }; - - beforeEach(() => { - spyOn(Cookies, 'set').and.callFake(() => {}); - spyOn(hidePopover, 'call').and.callFake(() => {}); - spyOn(context, 'hide').and.callFake(() => {}); - dismiss.call(context); - }); - - it('sets cookie to true', () => { - expect(Cookies.set).toHaveBeenCalled(); - }); - - it('calls hide popover', () => { - expect(hidePopover.call).toHaveBeenCalled(); - }); - - it('calls hide', () => { - expect(context.hide).toHaveBeenCalled(); - }); - }); - - describe('mouseleave', () => { - it('calls hide popover if .popover:hover is false', () => { - const fakeJquery = { - length: 0, - }; - - spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); - spyOn(hidePopover, 'call'); - mouseleave(); - expect(hidePopover.call).toHaveBeenCalled(); - }); - - it('does not call hide popover if .popover:hover is true', () => { - const fakeJquery = { - length: 1, - }; - - spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); - spyOn(hidePopover, 'call'); - mouseleave(); - expect(hidePopover.call).not.toHaveBeenCalled(); - }); - }); - - describe('mouseenter', () => { - const context = {}; - - it('shows popover', () => { - spyOn(showPopover, 'call').and.returnValue(false); - mouseenter.call(context); - expect(showPopover.call).toHaveBeenCalled(); - }); - - it('registers mouseleave event if popover is showed', (done) => { - spyOn(showPopover, 'call').and.returnValue(true); - spyOn($.fn, 'on').and.callFake((eventName) => { - expect(eventName).toEqual('mouseleave'); - done(); - }); - mouseenter.call(context); - }); - - it('does not register mouseleave event if popover is not showed', () => { - spyOn(showPopover, 'call').and.returnValue(false); - const spy = spyOn($.fn, 'on').and.callFake(() => {}); - mouseenter.call(context); - expect(spy).not.toHaveBeenCalled(); - }); - }); - - describe('setupDismissButton', () => { - it('registers click event callback', (done) => { - const context = { - getAttribute: () => 'popoverId', - dataset: { - highlight: 'cookieId', - }, - }; - - spyOn($.fn, 'on').and.callFake((event) => { - expect(event).toEqual('click'); - done(); - }); - setupDismissButton.call(context); - }); - }); -}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js deleted file mode 100644 index 7feb361edec..00000000000 --- a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import domContentLoaded from '~/feature_highlight/feature_highlight_options'; -import bp from '~/breakpoints'; - -describe('feature highlight options', () => { - describe('domContentLoaded', () => { - const highlightOrder = []; - - beforeEach(() => { - // Check for when highlightFeatures is called - spyOn(highlightOrder, 'find').and.callFake(() => {}); - }); - - it('should not call highlightFeatures when breakpoint is xs', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('xs'); - - domContentLoaded(highlightOrder); - expect(bp.getBreakpointSize).toHaveBeenCalled(); - expect(highlightOrder.find).not.toHaveBeenCalled(); - }); - - it('should not call highlightFeatures when breakpoint is sm', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); - - domContentLoaded(highlightOrder); - expect(bp.getBreakpointSize).toHaveBeenCalled(); - expect(highlightOrder.find).not.toHaveBeenCalled(); - }); - - it('should not call highlightFeatures when breakpoint is md', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('md'); - - domContentLoaded(highlightOrder); - expect(bp.getBreakpointSize).toHaveBeenCalled(); - expect(highlightOrder.find).not.toHaveBeenCalled(); - }); - - it('should call highlightFeatures when breakpoint is lg', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('lg'); - - domContentLoaded(highlightOrder); - expect(bp.getBreakpointSize).toHaveBeenCalled(); - expect(highlightOrder.find).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js deleted file mode 100644 index 6abe8425ee7..00000000000 --- a/spec/javascripts/feature_highlight/feature_highlight_spec.js +++ /dev/null @@ -1,122 +0,0 @@ -import Cookies from 'js-cookie'; -import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper'; -import * as featureHighlight from '~/feature_highlight/feature_highlight'; - -describe('feature highlight', () => { - describe('setupFeatureHighlightPopover', () => { - const selector = '.js-feature-highlight[data-highlight=test]'; - beforeEach(() => { - setFixtures(` - <div> - <div class="js-feature-highlight" data-highlight="test" disabled> - Trigger - </div> - </div> - <div class="feature-highlight-popover-content"> - Content - <div class="dismiss-feature-highlight"> - Dismiss - </div> - </div> - `); - spyOn(window, 'addEventListener'); - spyOn(window, 'removeEventListener'); - featureHighlight.setupFeatureHighlightPopover('test', 0); - }); - - it('setups popover content', () => { - const $popoverContent = $('.feature-highlight-popover-content'); - const outerHTML = $popoverContent.prop('outerHTML'); - - expect($(selector).data('content')).toEqual(outerHTML); - }); - - it('setups mouseenter', () => { - const showSpy = spyOn(featureHighlightHelper.showPopover, 'call'); - $(selector).trigger('mouseenter'); - - expect(showSpy).toHaveBeenCalled(); - }); - - it('setups debounced mouseleave', (done) => { - const hideSpy = spyOn(featureHighlightHelper.hidePopover, 'call'); - $(selector).trigger('mouseleave'); - - // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce - setTimeout(() => { - expect(hideSpy).toHaveBeenCalled(); - done(); - }, 0); - }); - - it('setups inserted.bs.popover', () => { - $(selector).trigger('mouseenter'); - const popoverId = $(selector).attr('aria-describedby'); - const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click'); - - $(`#${popoverId} .dismiss-feature-highlight`).click(); - expect(spyEvent).toHaveBeenTriggered(); - }); - - it('setups show.bs.popover', () => { - $(selector).trigger('show.bs.popover'); - expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); - }); - - it('setups hide.bs.popover', () => { - $(selector).trigger('hide.bs.popover'); - expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); - }); - - it('removes disabled attribute', () => { - expect($('.js-feature-highlight').is(':disabled')).toEqual(false); - }); - - it('displays popover', () => { - expect($(selector).attr('aria-describedby')).toBeFalsy(); - $(selector).trigger('mouseenter'); - expect($(selector).attr('aria-describedby')).toBeTruthy(); - }); - }); - - describe('shouldHighlightFeature', () => { - it('should return false if element is not found', () => { - spyOn(document, 'querySelector').and.returnValue(null); - spyOn(Cookies, 'get').and.returnValue(null); - - expect(featureHighlight.shouldHighlightFeature()).toBeFalsy(); - }); - - it('should return false if previouslyDismissed', () => { - spyOn(document, 'querySelector').and.returnValue(document.createElement('div')); - spyOn(Cookies, 'get').and.returnValue('true'); - - expect(featureHighlight.shouldHighlightFeature()).toBeFalsy(); - }); - - it('should return true if element is found and not previouslyDismissed', () => { - spyOn(document, 'querySelector').and.returnValue(document.createElement('div')); - spyOn(Cookies, 'get').and.returnValue(null); - - expect(featureHighlight.shouldHighlightFeature()).toBeTruthy(); - }); - }); - - describe('highlightFeatures', () => { - it('calls setupFeatureHighlightPopover if shouldHighlightFeature returns true', () => { - // Mimic shouldHighlightFeature set to true - const highlightOrder = ['issue-boards']; - spyOn(highlightOrder, 'find').and.returnValue(highlightOrder[0]); - - expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(true); - }); - - it('does not call setupFeatureHighlightPopover if shouldHighlightFeature returns false', () => { - // Mimic shouldHighlightFeature set to false - const highlightOrder = ['issue-boards']; - spyOn(highlightOrder, 'find').and.returnValue(null); - - expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(false); - }); - }); -}); diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index b3c9bca64cc..02415485d19 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -10,6 +10,7 @@ describe('Dropdown User', () => { beforeEach(() => { spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); dropdownUser = new gl.DropdownUser({ @@ -38,6 +39,7 @@ describe('Dropdown User', () => { beforeEach(() => { spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); }); it('should return endpoint', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 16ae649ee60..f209328dee1 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -411,4 +411,26 @@ describe('Filtered Search Manager', () => { expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false); }); }); + + describe('getAllParams', () => { + beforeEach(() => { + this.paramsArr = ['key=value', 'otherkey=othervalue']; + + initializeManager(); + }); + + it('correctly modifies params when custom modifier is passed', () => { + const modifedParams = manager.getAllParams.call({ + modifyUrlParams: paramsArr => paramsArr.reverse(), + }, [].concat(this.paramsArr)); + + expect(modifedParams[0]).toBe(this.paramsArr[1]); + }); + + it('does not modify params when no custom modifier is passed', () => { + const modifedParams = manager.getAllParams.call({}, this.paramsArr); + + expect(modifedParams[1]).toBe(this.paramsArr[1]); + }); + }); }); diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb new file mode 100644 index 00000000000..5774f36f026 --- /dev/null +++ b/spec/javascripts/fixtures/clusters.rb @@ -0,0 +1,34 @@ +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) { project.create_cluster!(gcp_cluster_name: "gke-test-creation-1", gcp_project_id: 'gitlab-internal-153318', gcp_cluster_zone: 'us-central1-a', gcp_cluster_size: '1', project_namespace: 'aaa', gcp_machine_type: 'n1-standard-1')} + + 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.raw' do |example| + get :show, + namespace_id: project.namespace.to_param, + project_id: project, + id: cluster + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/dashboard.rb b/spec/javascripts/fixtures/dashboard.rb deleted file mode 100644 index 7fa351680c9..00000000000 --- a/spec/javascripts/fixtures/dashboard.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -describe Dashboard::ProjectsController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, namespace: namespace, path: 'builds-project') } - - render_views - - before(:all) do - clean_frontend_fixtures('dashboard/') - end - - before do - sign_in(admin) - end - - after do - remove_repository(project) - end - - it 'dashboard/user-callout.html.raw' do |example| - rendered = render_template('shared/_user_callout') - store_frontend_fixture(rendered, example.description) - end - - private - - def render_template(template_file_name) - controller.prepend_view_path(JavaScriptFixturesHelpers::FIXTURE_PATH) - controller.render_to_string(template_file_name, layout: false) - end -end diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 4bc2205e642..3fd16d76f51 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -41,6 +41,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont remove_repository(project) end + it 'merge_requests/merge_request_of_current_user.html.raw' do |example| + merge_request.update(author: admin) + + render_merge_request(example.description, merge_request) + end + it 'merge_requests/merge_request_with_task_list.html.raw' do |example| create(:ci_build, :pending, pipeline: pipeline) diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml index 418a38a0e2e..97b0c25c923 100644 --- a/spec/javascripts/fixtures/pipelines.html.haml +++ b/spec/javascripts/fixtures/pipelines.html.haml @@ -2,6 +2,8 @@ #pipelines-list-vue{ data: { endpoint: 'foo', "css-class" => 'foo', "help-page-path" => 'foo', + "empty-state-svg-path" => 'foo', + "error-state-svg-path" => 'foo', "new-pipeline-path" => 'foo', "can-create-pipeline" => 'true', "all-path" => 'foo', diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index f8b37c0edde..4f20e31f511 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -73,6 +73,12 @@ describe('Fly out sidebar navigation', () => { ).toBe(0); }); + it('returns 0 if mousePos is empty', () => { + expect( + getHideSubItemsInterval(), + ).toBe(0); + }); + it('returns 0 when mouse above sub-items', () => { showSubLevelItems(el); documentMouseMove({ @@ -271,12 +277,19 @@ describe('Fly out sidebar navigation', () => { }); it('sets transform of sub-items', () => { + const sidebar = document.createElement('div'); const subItems = el.querySelector('.sidebar-sub-level-items'); + + sidebar.style.width = '200px'; + + document.body.appendChild(sidebar); + + setSidebar(sidebar); showSubLevelItems(el); expect( subItems.style.transform, - ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`); + ).toBe(`translate3d(200px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`); }); it('sets is-above when element is above', () => { diff --git a/spec/javascripts/notes/stores/helpers.js b/spec/javascripts/helpers/vuex_action_helper.js index 2d386fe1da5..2d386fe1da5 100644 --- a/spec/javascripts/notes/stores/helpers.js +++ b/spec/javascripts/helpers/vuex_action_helper.js diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js new file mode 100644 index 00000000000..fb9c7e59031 --- /dev/null +++ b/spec/javascripts/image_diff/helpers/badge_helper_spec.js @@ -0,0 +1,132 @@ +import * as badgeHelper from '~/image_diff/helpers/badge_helper'; +import * as mockData from '../mock_data'; + +describe('badge helper', () => { + const { coordinate, noteId, badgeText, badgeNumber } = mockData; + let containerEl; + let buttonEl; + + beforeEach(() => { + containerEl = document.createElement('div'); + }); + + describe('createImageBadge', () => { + beforeEach(() => { + buttonEl = badgeHelper.createImageBadge(noteId, coordinate); + }); + + it('should create button', () => { + expect(buttonEl.tagName).toEqual('BUTTON'); + expect(buttonEl.getAttribute('type')).toEqual('button'); + }); + + it('should set disabled attribute', () => { + expect(buttonEl.hasAttribute('disabled')).toEqual(true); + }); + + it('should set noteId', () => { + expect(buttonEl.dataset.noteId).toEqual(noteId); + }); + + it('should set coordinate', () => { + expect(buttonEl.style.left).toEqual(`${coordinate.x}px`); + expect(buttonEl.style.top).toEqual(`${coordinate.y}px`); + }); + + describe('classNames', () => { + it('should set .js-image-badge by default', () => { + expect(buttonEl.className).toEqual('js-image-badge'); + }); + + it('should add additional class names if parameter is passed', () => { + const classNames = ['first-class', 'second-class']; + buttonEl = badgeHelper.createImageBadge(noteId, coordinate, classNames); + + expect(buttonEl.className).toEqual(classNames.concat('js-image-badge').join(' ')); + }); + }); + }); + + describe('addImageBadge', () => { + beforeEach(() => { + badgeHelper.addImageBadge(containerEl, { + coordinate, + badgeText, + noteId, + }); + buttonEl = containerEl.querySelector('button'); + }); + + it('should appends button to container', () => { + expect(buttonEl).toBeDefined(); + }); + + it('should set the badge text', () => { + expect(buttonEl.innerText).toEqual(badgeText); + }); + + it('should set the button coordinates', () => { + expect(buttonEl.style.left).toEqual(`${coordinate.x}px`); + expect(buttonEl.style.top).toEqual(`${coordinate.y}px`); + }); + + it('should set the button noteId', () => { + expect(buttonEl.dataset.noteId).toEqual(noteId); + }); + }); + + describe('addImageCommentBadge', () => { + beforeEach(() => { + badgeHelper.addImageCommentBadge(containerEl, { + coordinate, + noteId, + }); + buttonEl = containerEl.querySelector('button'); + }); + + it('should append icon button to container', () => { + expect(buttonEl).toBeDefined(); + }); + + it('should create icon comment button', () => { + const iconEl = buttonEl.querySelector('i'); + 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); + }); + }); + + describe('addAvatarBadge', () => { + let avatarBadgeEl; + + beforeEach(() => { + containerEl.innerHTML = ` + <div id="${noteId}"> + <div class="badge hidden"> + </div> + </div> + `; + + badgeHelper.addAvatarBadge(containerEl, { + detail: { + noteId, + badgeNumber, + }, + }); + avatarBadgeEl = containerEl.querySelector(`#${noteId} .badge`); + }); + + it('should update badge number', () => { + expect(avatarBadgeEl.innerText).toEqual(badgeNumber.toString()); + }); + + it('should remove hidden class', () => { + expect(avatarBadgeEl.classList.contains('hidden')).toEqual(false); + }); + }); +}); diff --git a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js new file mode 100644 index 00000000000..a284b981d2a --- /dev/null +++ b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js @@ -0,0 +1,139 @@ +import * as commentIndicatorHelper from '~/image_diff/helpers/comment_indicator_helper'; +import * as mockData from '../mock_data'; + +describe('commentIndicatorHelper', () => { + const { coordinate } = mockData; + let containerEl; + + beforeEach(() => { + containerEl = document.createElement('div'); + }); + + describe('addCommentIndicator', () => { + let buttonEl; + + beforeEach(() => { + commentIndicatorHelper.addCommentIndicator(containerEl, coordinate); + buttonEl = containerEl.querySelector('button'); + }); + + it('should append button to container', () => { + expect(buttonEl).toBeDefined(); + }); + + describe('button', () => { + it('should set coordinate', () => { + expect(buttonEl.style.left).toEqual(`${coordinate.x}px`); + expect(buttonEl.style.top).toEqual(`${coordinate.y}px`); + }); + + it('should contain image-comment-dark svg', () => { + const svgEl = buttonEl.querySelector('svg'); + expect(svgEl).toBeDefined(); + + const svgLink = svgEl.querySelector('use').getAttribute('xlink:href'); + expect(svgLink.indexOf('image-comment-dark') !== -1).toEqual(true); + }); + }); + }); + + describe('removeCommentIndicator', () => { + it('should return removed false if there is no comment-indicator', () => { + const result = commentIndicatorHelper.removeCommentIndicator(containerEl); + expect(result.removed).toEqual(false); + }); + + describe('has comment indicator', () => { + let result; + + beforeEach(() => { + containerEl.innerHTML = ` + <div class="comment-indicator" style="left:${coordinate.x}px; top: ${coordinate.y}px;"> + <img src="${gl.TEST_HOST}/image.png"> + </div> + `; + result = commentIndicatorHelper.removeCommentIndicator(containerEl); + }); + + it('should remove comment indicator', () => { + expect(containerEl.querySelector('.comment-indicator')).toBeNull(); + }); + + it('should return removed true', () => { + expect(result.removed).toEqual(true); + }); + + it('should return indicator meta', () => { + expect(result.x).toEqual(coordinate.x); + expect(result.y).toEqual(coordinate.y); + expect(result.image).toBeDefined(); + expect(result.image.width).toBeDefined(); + expect(result.image.height).toBeDefined(); + }); + }); + }); + + describe('showCommentIndicator', () => { + describe('commentIndicator exists', () => { + beforeEach(() => { + containerEl.innerHTML = ` + <button class="comment-indicator"></button> + `; + commentIndicatorHelper.showCommentIndicator(containerEl, coordinate); + }); + + it('should set commentIndicator coordinates', () => { + const commentIndicatorEl = containerEl.querySelector('.comment-indicator'); + expect(commentIndicatorEl.style.left).toEqual(`${coordinate.x}px`); + expect(commentIndicatorEl.style.top).toEqual(`${coordinate.y}px`); + }); + }); + + describe('commentIndicator does not exist', () => { + beforeEach(() => { + commentIndicatorHelper.showCommentIndicator(containerEl, coordinate); + }); + + it('should addCommentIndicator', () => { + const buttonEl = containerEl.querySelector('.comment-indicator'); + expect(buttonEl).toBeDefined(); + expect(buttonEl.style.left).toEqual(`${coordinate.x}px`); + expect(buttonEl.style.top).toEqual(`${coordinate.y}px`); + }); + }); + }); + + describe('commentIndicatorOnClick', () => { + let event; + let textAreaEl; + + beforeEach(() => { + containerEl.innerHTML = ` + <div class="diff-viewer"> + <button></button> + <div class="note-container"> + <textarea class="note-textarea"></textarea> + </div> + </div> + `; + textAreaEl = containerEl.querySelector('textarea'); + + event = { + stopPropagation: () => {}, + currentTarget: containerEl.querySelector('button'), + }; + + spyOn(event, 'stopPropagation'); + spyOn(textAreaEl, 'focus'); + commentIndicatorHelper.commentIndicatorOnClick(event); + }); + + it('should stopPropagation', () => { + expect(event.stopPropagation).toHaveBeenCalled(); + }); + + it('should focus textAreaEl', () => { + expect(textAreaEl.focus).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/image_diff/helpers/dom_helper_spec.js b/spec/javascripts/image_diff/helpers/dom_helper_spec.js new file mode 100644 index 00000000000..8dde924e8ae --- /dev/null +++ b/spec/javascripts/image_diff/helpers/dom_helper_spec.js @@ -0,0 +1,118 @@ +import * as domHelper from '~/image_diff/helpers/dom_helper'; +import * as mockData from '../mock_data'; + +describe('domHelper', () => { + const { imageMeta, badgeNumber } = mockData; + + describe('setPositionDataAttribute', () => { + let containerEl; + let attributeAfterCall; + const position = { + myProperty: 'myProperty', + }; + + beforeEach(() => { + containerEl = document.createElement('div'); + containerEl.dataset.position = JSON.stringify(position); + domHelper.setPositionDataAttribute(containerEl, imageMeta); + attributeAfterCall = JSON.parse(containerEl.dataset.position); + }); + + it('should set x, y, width, height', () => { + expect(attributeAfterCall.x).toEqual(imageMeta.x); + expect(attributeAfterCall.y).toEqual(imageMeta.y); + expect(attributeAfterCall.width).toEqual(imageMeta.width); + expect(attributeAfterCall.height).toEqual(imageMeta.height); + }); + + it('should not override other properties', () => { + expect(attributeAfterCall.myProperty).toEqual('myProperty'); + }); + }); + + describe('updateDiscussionAvatarBadgeNumber', () => { + let discussionEl; + + beforeEach(() => { + discussionEl = document.createElement('div'); + discussionEl.innerHTML = ` + <a href="#" class="image-diff-avatar-link"> + <div class="badge"></div> + </a> + `; + domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber); + }); + + it('should update avatar badge number', () => { + expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString()); + }); + }); + + describe('updateDiscussionBadgeNumber', () => { + let discussionEl; + + beforeEach(() => { + discussionEl = document.createElement('div'); + discussionEl.innerHTML = ` + <div class="badge"></div> + `; + domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber); + }); + + it('should update discussion badge number', () => { + expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString()); + }); + }); + + describe('toggleCollapsed', () => { + let element; + let discussionNotesEl; + + beforeEach(() => { + element = document.createElement('div'); + element.innerHTML = ` + <div class="discussion-notes"> + <button></button> + <form class="discussion-form"></form> + </div> + `; + discussionNotesEl = element.querySelector('.discussion-notes'); + }); + + describe('not collapsed', () => { + beforeEach(() => { + domHelper.toggleCollapsed({ + currentTarget: element.querySelector('button'), + }); + }); + + it('should add collapsed class', () => { + expect(discussionNotesEl.classList.contains('collapsed')).toEqual(true); + }); + + it('should force formEl to display none', () => { + const formEl = element.querySelector('.discussion-form'); + expect(formEl.style.display).toEqual('none'); + }); + }); + + describe('collapsed', () => { + beforeEach(() => { + discussionNotesEl.classList.add('collapsed'); + + domHelper.toggleCollapsed({ + currentTarget: element.querySelector('button'), + }); + }); + + it('should remove collapsed class', () => { + expect(discussionNotesEl.classList.contains('collapsed')).toEqual(false); + }); + + it('should force formEl to display block', () => { + const formEl = element.querySelector('.discussion-form'); + expect(formEl.style.display).toEqual('block'); + }); + }); + }); +}); diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/javascripts/image_diff/helpers/utils_helper_spec.js new file mode 100644 index 00000000000..56d77a05c4c --- /dev/null +++ b/spec/javascripts/image_diff/helpers/utils_helper_spec.js @@ -0,0 +1,207 @@ +import * as utilsHelper from '~/image_diff/helpers/utils_helper'; +import ImageDiff from '~/image_diff/image_diff'; +import ReplacedImageDiff from '~/image_diff/replaced_image_diff'; +import ImageBadge from '~/image_diff/image_badge'; +import * as mockData from '../mock_data'; + +describe('utilsHelper', () => { + const { + noteId, + discussionId, + image, + imageProperties, + imageMeta, + } = mockData; + + describe('resizeCoordinatesToImageElement', () => { + let result; + + beforeEach(() => { + result = utilsHelper.resizeCoordinatesToImageElement(image, imageMeta); + }); + + it('should return x based on widthRatio', () => { + expect(result.x).toEqual(imageMeta.x * 0.5); + }); + + it('should return y based on heightRatio', () => { + expect(result.y).toEqual(imageMeta.y * 0.5); + }); + + it('should return image width', () => { + expect(result.width).toEqual(image.width); + }); + + it('should return image height', () => { + expect(result.height).toEqual(image.height); + }); + }); + + describe('generateBadgeFromDiscussionDOM', () => { + let discussionEl; + let result; + + beforeEach(() => { + const imageFrameEl = document.createElement('div'); + imageFrameEl.innerHTML = ` + <img src="${gl.TEST_HOST}/image.png"> + `; + discussionEl = document.createElement('div'); + discussionEl.dataset.discussionId = discussionId; + discussionEl.innerHTML = ` + <div class="note" id="${noteId}"></div> + `; + discussionEl.dataset.position = JSON.stringify(imageMeta); + result = utilsHelper.generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl); + }); + + it('should return actual image properties', () => { + const { actual } = result; + expect(actual.x).toEqual(imageMeta.x); + expect(actual.y).toEqual(imageMeta.y); + expect(actual.width).toEqual(imageMeta.width); + expect(actual.height).toEqual(imageMeta.height); + }); + + it('should return browser image properties', () => { + const { browser } = result; + expect(browser.x).toBeDefined(); + expect(browser.y).toBeDefined(); + expect(browser.width).toBeDefined(); + expect(browser.height).toBeDefined(); + }); + + it('should return instance of ImageBadge', () => { + expect(result instanceof ImageBadge).toEqual(true); + }); + + it('should return noteId', () => { + expect(result.noteId).toEqual(noteId); + }); + + it('should return discussionId', () => { + expect(result.discussionId).toEqual(discussionId); + }); + }); + + describe('getTargetSelection', () => { + let containerEl; + + beforeEach(() => { + containerEl = { + querySelector: () => imageProperties, + }; + }); + + function generateEvent(offsetX, offsetY) { + return { + currentTarget: containerEl, + offsetX, + offsetY, + }; + } + + it('should return browser properties', () => { + const event = generateEvent(25, 25); + const result = utilsHelper.getTargetSelection(event); + + const { browser } = result; + expect(browser.x).toEqual(event.offsetX); + expect(browser.y).toEqual(event.offsetY); + expect(browser.width).toEqual(imageProperties.width); + expect(browser.height).toEqual(imageProperties.height); + }); + + it('should return resized actual image properties', () => { + const event = generateEvent(50, 50); + const result = utilsHelper.getTargetSelection(event); + + const { actual } = result; + expect(actual.x).toEqual(100); + expect(actual.y).toEqual(100); + expect(actual.width).toEqual(imageProperties.naturalWidth); + expect(actual.height).toEqual(imageProperties.naturalHeight); + }); + + describe('normalize coordinates', () => { + it('should return x = 0 if x < 0', () => { + const event = generateEvent(-5, 50); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.x).toEqual(0); + }); + + it('should return x = width if x > width', () => { + const event = generateEvent(1000, 50); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.x).toEqual(imageProperties.width); + }); + + it('should return y = 0 if y < 0', () => { + const event = generateEvent(50, -10); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.y).toEqual(0); + }); + + it('should return y = height if y > height', () => { + const event = generateEvent(50, 1000); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.y).toEqual(imageProperties.height); + }); + }); + }); + + describe('initImageDiff', () => { + let glCache; + let fileEl; + + beforeEach(() => { + window.gl = window.gl || (window.gl = {}); + glCache = window.gl; + window.gl.ImageFile = () => {}; + fileEl = document.createElement('div'); + fileEl.innerHTML = ` + <div class="diff-file"></div> + `; + + spyOn(ImageDiff.prototype, 'init').and.callFake(() => {}); + spyOn(ReplacedImageDiff.prototype, 'init').and.callFake(() => {}); + }); + + afterEach(() => { + window.gl = glCache; + }); + + it('should initialize gl.ImageFile', () => { + spyOn(window.gl, 'ImageFile'); + + utilsHelper.initImageDiff(fileEl, false, false); + expect(gl.ImageFile).toHaveBeenCalled(); + }); + + it('should initialize ImageDiff if js-single-image', () => { + const diffFileEl = fileEl.querySelector('.diff-file'); + diffFileEl.innerHTML = ` + <div class="js-single-image"> + </div> + `; + + const imageDiff = utilsHelper.initImageDiff(fileEl, true, false); + expect(ImageDiff.prototype.init).toHaveBeenCalled(); + expect(imageDiff.canCreateNote).toEqual(true); + expect(imageDiff.renderCommentBadge).toEqual(false); + }); + + it('should initialize ReplacedImageDiff if js-replaced-image', () => { + const diffFileEl = fileEl.querySelector('.diff-file'); + diffFileEl.innerHTML = ` + <div class="js-replaced-image"> + </div> + `; + + const replacedImageDiff = utilsHelper.initImageDiff(fileEl, false, true); + expect(ReplacedImageDiff.prototype.init).toHaveBeenCalled(); + expect(replacedImageDiff.canCreateNote).toEqual(false); + expect(replacedImageDiff.renderCommentBadge).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/image_diff/image_badge_spec.js b/spec/javascripts/image_diff/image_badge_spec.js new file mode 100644 index 00000000000..87f98fc0926 --- /dev/null +++ b/spec/javascripts/image_diff/image_badge_spec.js @@ -0,0 +1,84 @@ +import ImageBadge from '~/image_diff/image_badge'; +import imageDiffHelper from '~/image_diff/helpers/index'; +import * as mockData from './mock_data'; + +describe('ImageBadge', () => { + const { noteId, discussionId, imageMeta } = mockData; + const options = { + noteId, + discussionId, + }; + + it('should save actual property', () => { + const imageBadge = new ImageBadge(Object.assign({}, options, { + actual: imageMeta, + })); + + const { actual } = imageBadge; + expect(actual.x).toEqual(imageMeta.x); + expect(actual.y).toEqual(imageMeta.y); + expect(actual.width).toEqual(imageMeta.width); + expect(actual.height).toEqual(imageMeta.height); + }); + + it('should save browser property', () => { + const imageBadge = new ImageBadge(Object.assign({}, options, { + browser: imageMeta, + })); + + const { browser } = imageBadge; + expect(browser.x).toEqual(imageMeta.x); + expect(browser.y).toEqual(imageMeta.y); + expect(browser.width).toEqual(imageMeta.width); + expect(browser.height).toEqual(imageMeta.height); + }); + + it('should save noteId', () => { + const imageBadge = new ImageBadge(options); + expect(imageBadge.noteId).toEqual(noteId); + }); + + it('should save discussionId', () => { + const imageBadge = new ImageBadge(options); + expect(imageBadge.discussionId).toEqual(discussionId); + }); + + describe('default values', () => { + let imageBadge; + + beforeEach(() => { + imageBadge = new ImageBadge(options); + }); + + it('should return defaultimageMeta if actual property is not provided', () => { + const { actual } = imageBadge; + expect(actual.x).toEqual(0); + expect(actual.y).toEqual(0); + expect(actual.width).toEqual(0); + expect(actual.height).toEqual(0); + }); + + it('should return defaultimageMeta if browser property is not provided', () => { + const { browser } = imageBadge; + expect(browser.x).toEqual(0); + expect(browser.y).toEqual(0); + expect(browser.width).toEqual(0); + expect(browser.height).toEqual(0); + }); + }); + + describe('imageEl property is provided and not browser property', () => { + beforeEach(() => { + spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(true); + }); + + it('should generate browser property', () => { + const imageBadge = new ImageBadge(Object.assign({}, options, { + imageEl: document.createElement('img'), + })); + + expect(imageDiffHelper.resizeCoordinatesToImageElement).toHaveBeenCalled(); + expect(imageBadge.browser).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/image_diff/image_diff_spec.js b/spec/javascripts/image_diff/image_diff_spec.js new file mode 100644 index 00000000000..346282328c7 --- /dev/null +++ b/spec/javascripts/image_diff/image_diff_spec.js @@ -0,0 +1,361 @@ +import ImageDiff from '~/image_diff/image_diff'; +import * as imageUtility from '~/lib/utils/image_utility'; +import imageDiffHelper from '~/image_diff/helpers/index'; +import * as mockData from './mock_data'; + +describe('ImageDiff', () => { + let element; + let imageDiff; + + beforeEach(() => { + setFixtures(` + <div id="element"> + <div class="diff-file"> + <div class="js-image-frame"> + <img src="${gl.TEST_HOST}/image.png"> + <div class="comment-indicator"></div> + <div id="badge-1" class="badge">1</div> + <div id="badge-2" class="badge">2</div> + <div id="badge-3" class="badge">3</div> + </div> + <div class="note-container"> + <div class="discussion-notes"> + <div class="js-diff-notes-toggle"></div> + <div class="notes"></div> + </div> + <div class="discussion-notes"> + <div class="js-diff-notes-toggle"></div> + <div class="notes"></div> + </div> + </div> + </div> + </div> + `); + element = document.getElementById('element'); + }); + + describe('constructor', () => { + beforeEach(() => { + imageDiff = new ImageDiff(element, { + canCreateNote: true, + renderCommentBadge: true, + }); + }); + + it('should set el', () => { + expect(imageDiff.el).toEqual(element); + }); + + it('should set canCreateNote', () => { + expect(imageDiff.canCreateNote).toEqual(true); + }); + + it('should set renderCommentBadge', () => { + expect(imageDiff.renderCommentBadge).toEqual(true); + }); + + it('should set $noteContainer', () => { + expect(imageDiff.$noteContainer[0]).toEqual(element.querySelector('.note-container')); + }); + + describe('default', () => { + beforeEach(() => { + imageDiff = new ImageDiff(element); + }); + + it('should set canCreateNote as false', () => { + expect(imageDiff.canCreateNote).toEqual(false); + }); + + it('should set renderCommentBadge as false', () => { + expect(imageDiff.renderCommentBadge).toEqual(false); + }); + }); + }); + + describe('init', () => { + beforeEach(() => { + spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.init(); + }); + + it('should set imageFrameEl', () => { + expect(imageDiff.imageFrameEl).toEqual(element.querySelector('.diff-file .js-image-frame')); + }); + + it('should set imageEl', () => { + expect(imageDiff.imageEl).toEqual(element.querySelector('.diff-file .js-image-frame img')); + }); + + it('should call bindEvents', () => { + expect(imageDiff.bindEvents).toHaveBeenCalled(); + }); + }); + + describe('bindEvents', () => { + let imageEl; + + beforeEach(() => { + spyOn(imageDiffHelper, 'toggleCollapsed').and.callFake(() => {}); + spyOn(imageDiffHelper, 'commentIndicatorOnClick').and.callFake(() => {}); + spyOn(imageDiffHelper, 'removeCommentIndicator').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'imageClicked').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'addBadge').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'removeBadge').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'renderBadges').and.callFake(() => {}); + imageEl = element.querySelector('.diff-file .js-image-frame img'); + }); + + describe('default', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should register click event delegation to js-diff-notes-toggle', () => { + element.querySelector('.js-diff-notes-toggle').click(); + expect(imageDiffHelper.toggleCollapsed).toHaveBeenCalled(); + }); + + it('should register click event delegation to comment-indicator', () => { + element.querySelector('.comment-indicator').click(); + expect(imageDiffHelper.commentIndicatorOnClick).toHaveBeenCalled(); + }); + }); + + describe('image loaded', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(true); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + }); + + it('should renderBadges', () => {}); + }); + + describe('image not loaded', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should registers load eventListener', () => { + const loadEvent = new Event('load'); + imageEl.dispatchEvent(loadEvent); + expect(imageDiff.renderBadges).toHaveBeenCalled(); + }); + }); + + describe('canCreateNote', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element, { + canCreateNote: true, + }); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should register click.imageDiff event', () => { + const event = new CustomEvent('click.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.imageClicked).toHaveBeenCalled(); + }); + + it('should register blur.imageDiff event', () => { + const event = new CustomEvent('blur.imageDiff'); + element.dispatchEvent(event); + expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled(); + }); + + it('should register addBadge.imageDiff event', () => { + const event = new CustomEvent('addBadge.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.addBadge).toHaveBeenCalled(); + }); + + it('should register removeBadge.imageDiff event', () => { + const event = new CustomEvent('removeBadge.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.removeBadge).toHaveBeenCalled(); + }); + }); + + describe('canCreateNote is false', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should not register click.imageDiff event', () => { + const event = new CustomEvent('click.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.imageClicked).not.toHaveBeenCalled(); + }); + }); + }); + + describe('imageClicked', () => { + beforeEach(() => { + spyOn(imageDiffHelper, 'getTargetSelection').and.returnValue({ + actual: {}, + browser: {}, + }); + spyOn(imageDiffHelper, 'setPositionDataAttribute').and.callFake(() => {}); + spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.imageClicked({ + detail: { + currentTarget: {}, + }, + }); + }); + + it('should call getTargetSelection', () => { + expect(imageDiffHelper.getTargetSelection).toHaveBeenCalled(); + }); + + it('should call setPositionDataAttribute', () => { + expect(imageDiffHelper.setPositionDataAttribute).toHaveBeenCalled(); + }); + + it('should call showCommentIndicator', () => { + expect(imageDiffHelper.showCommentIndicator).toHaveBeenCalled(); + }); + }); + + describe('renderBadges', () => { + beforeEach(() => { + spyOn(ImageDiff.prototype, 'renderBadge').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.renderBadges(); + }); + + it('should call renderBadge for each discussionEl', () => { + const discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes'); + expect(imageDiff.renderBadge.calls.count()).toEqual(discussionEls.length); + }); + }); + + describe('renderBadge', () => { + let discussionEls; + + beforeEach(() => { + spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'addImageCommentBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').and.returnValue({ + browser: {}, + noteId: 'noteId', + }); + discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes'); + imageDiff = new ImageDiff(element); + imageDiff.renderBadge(discussionEls[0], 0); + }); + + it('should populate imageBadges', () => { + expect(imageDiff.imageBadges.length).toEqual(1); + }); + + describe('renderCommentBadge', () => { + beforeEach(() => { + imageDiff.renderCommentBadge = true; + imageDiff.renderBadge(discussionEls[0], 0); + }); + + it('should call addImageCommentBadge', () => { + expect(imageDiffHelper.addImageCommentBadge).toHaveBeenCalled(); + }); + }); + + describe('renderCommentBadge is false', () => { + it('should call addImageBadge', () => { + expect(imageDiffHelper.addImageBadge).toHaveBeenCalled(); + }); + }); + }); + + describe('addBadge', () => { + beforeEach(() => { + spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'addAvatarBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame'); + imageDiff.addBadge({ + detail: { + x: 0, + y: 1, + width: 25, + height: 50, + noteId: 'noteId', + discussionId: 'discussionId', + }, + }); + }); + + it('should add imageBadge to imageBadges', () => { + expect(imageDiff.imageBadges.length).toEqual(1); + }); + + it('should call addImageBadge', () => { + expect(imageDiffHelper.addImageBadge).toHaveBeenCalled(); + }); + + it('should call addAvatarBadge', () => { + expect(imageDiffHelper.addAvatarBadge).toHaveBeenCalled(); + }); + + it('should call updateDiscussionBadgeNumber', () => { + expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled(); + }); + }); + + describe('removeBadge', () => { + beforeEach(() => { + const { imageMeta } = mockData; + + spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {}); + spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.imageBadges = [imageMeta, imageMeta, imageMeta]; + imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame'); + imageDiff.removeBadge({ + detail: { + badgeNumber: 2, + }, + }); + }); + + describe('cascade badge count', () => { + it('should update next imageBadgeEl value', () => { + const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge'); + expect(imageBadgeEls[0].innerText).toEqual('1'); + expect(imageBadgeEls[1].innerText).toEqual('2'); + expect(imageBadgeEls.length).toEqual(2); + }); + + it('should call updateDiscussionBadgeNumber', () => { + expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled(); + }); + + it('should call updateDiscussionAvatarBadgeNumber', () => { + expect(imageDiffHelper.updateDiscussionAvatarBadgeNumber).toHaveBeenCalled(); + }); + }); + + it('should remove badge from imageBadges', () => { + expect(imageDiff.imageBadges.length).toEqual(2); + }); + + it('should remove imageBadgeEl', () => { + expect(imageDiff.imageFrameEl.querySelector('#badge-2')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/image_diff/init_discussion_tab_spec.js b/spec/javascripts/image_diff/init_discussion_tab_spec.js new file mode 100644 index 00000000000..7c447d6f70d --- /dev/null +++ b/spec/javascripts/image_diff/init_discussion_tab_spec.js @@ -0,0 +1,37 @@ +import initDiscussionTab from '~/image_diff/init_discussion_tab'; +import imageDiffHelper from '~/image_diff/helpers/index'; + +describe('initDiscussionTab', () => { + beforeEach(() => { + setFixtures(` + <div class="timeline-content"> + <div class="diff-file js-image-file"></div> + <div class="diff-file js-image-file"></div> + </div> + `); + }); + + it('should pass canCreateNote as false to initImageDiff', (done) => { + spyOn(imageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote) => { + expect(canCreateNote).toEqual(false); + done(); + }); + + initDiscussionTab(); + }); + + it('should pass renderCommentBadge as true to initImageDiff', (done) => { + spyOn(imageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote, renderCommentBadge) => { + expect(renderCommentBadge).toEqual(true); + done(); + }); + + initDiscussionTab(); + }); + + it('should call initImageDiff for each diffFileEls', () => { + spyOn(imageDiffHelper, 'initImageDiff').and.callFake(() => {}); + initDiscussionTab(); + expect(imageDiffHelper.initImageDiff.calls.count()).toEqual(2); + }); +}); diff --git a/spec/javascripts/image_diff/mock_data.js b/spec/javascripts/image_diff/mock_data.js new file mode 100644 index 00000000000..a0d1732dd0a --- /dev/null +++ b/spec/javascripts/image_diff/mock_data.js @@ -0,0 +1,28 @@ +export const noteId = 'noteId'; +export const discussionId = 'discussionId'; +export const badgeText = 'badgeText'; +export const badgeNumber = 5; + +export const coordinate = { + x: 100, + y: 100, +}; + +export const image = { + width: 100, + height: 100, +}; + +export const imageProperties = { + width: image.width, + height: image.height, + naturalWidth: image.width * 2, + naturalHeight: image.height * 2, +}; + +export const imageMeta = { + x: coordinate.x, + y: coordinate.y, + width: imageProperties.naturalWidth, + height: imageProperties.naturalHeight, +}; diff --git a/spec/javascripts/image_diff/replaced_image_diff_spec.js b/spec/javascripts/image_diff/replaced_image_diff_spec.js new file mode 100644 index 00000000000..5f8cd7c531a --- /dev/null +++ b/spec/javascripts/image_diff/replaced_image_diff_spec.js @@ -0,0 +1,312 @@ +import ReplacedImageDiff from '~/image_diff/replaced_image_diff'; +import ImageDiff from '~/image_diff/image_diff'; +import { viewTypes } from '~/image_diff/view_types'; +import imageDiffHelper from '~/image_diff/helpers/index'; + +describe('ReplacedImageDiff', () => { + let element; + let replacedImageDiff; + + beforeEach(() => { + setFixtures(` + <div id="element"> + <div class="two-up"> + <div class="js-image-frame"> + <img src="${gl.TEST_HOST}/image.png"> + </div> + </div> + <div class="swipe"> + <div class="js-image-frame"> + <img src="${gl.TEST_HOST}/image.png"> + </div> + </div> + <div class="onion-skin"> + <div class="js-image-frame"> + <img src="${gl.TEST_HOST}/image.png"> + </div> + </div> + <div class="view-modes-menu"> + <div class="two-up">2-up</div> + <div class="swipe">Swipe</div> + <div class="onion-skin">Onion skin</div> + </div> + </div> + `); + element = document.getElementById('element'); + }); + + function setupImageFrameEls() { + replacedImageDiff.imageFrameEls = []; + replacedImageDiff.imageFrameEls[viewTypes.TWO_UP] = element.querySelector('.two-up .js-image-frame'); + replacedImageDiff.imageFrameEls[viewTypes.SWIPE] = element.querySelector('.swipe .js-image-frame'); + replacedImageDiff.imageFrameEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin .js-image-frame'); + } + + function setupViewModesEls() { + replacedImageDiff.viewModesEls = []; + replacedImageDiff.viewModesEls[viewTypes.TWO_UP] = element.querySelector('.view-modes-menu .two-up'); + replacedImageDiff.viewModesEls[viewTypes.SWIPE] = element.querySelector('.view-modes-menu .swipe'); + replacedImageDiff.viewModesEls[viewTypes.ONION_SKIN] = element.querySelector('.view-modes-menu .onion-skin'); + } + + function setupImageEls() { + replacedImageDiff.imageEls = []; + replacedImageDiff.imageEls[viewTypes.TWO_UP] = element.querySelector('.two-up img'); + replacedImageDiff.imageEls[viewTypes.SWIPE] = element.querySelector('.swipe img'); + replacedImageDiff.imageEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin img'); + } + + it('should extend ImageDiff', () => { + replacedImageDiff = new ReplacedImageDiff(element); + expect(replacedImageDiff instanceof ImageDiff).toEqual(true); + }); + + describe('init', () => { + beforeEach(() => { + spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(ReplacedImageDiff.prototype, 'generateImageEls').and.callFake(() => {}); + + replacedImageDiff = new ReplacedImageDiff(element); + replacedImageDiff.init(); + }); + + it('should set imageFrameEls', () => { + const { imageFrameEls } = replacedImageDiff; + expect(imageFrameEls).toBeDefined(); + expect(imageFrameEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up .js-image-frame')); + expect(imageFrameEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe .js-image-frame')); + expect(imageFrameEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin .js-image-frame')); + }); + + it('should set viewModesEls', () => { + const { viewModesEls } = replacedImageDiff; + expect(viewModesEls).toBeDefined(); + expect(viewModesEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.view-modes-menu .two-up')); + expect(viewModesEls[viewTypes.SWIPE]).toEqual(element.querySelector('.view-modes-menu .swipe')); + expect(viewModesEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.view-modes-menu .onion-skin')); + }); + + it('should generateImageEls', () => { + expect(ReplacedImageDiff.prototype.generateImageEls).toHaveBeenCalled(); + }); + + it('should bindEvents', () => { + expect(ReplacedImageDiff.prototype.bindEvents).toHaveBeenCalled(); + }); + + describe('currentView', () => { + it('should set currentView', () => { + replacedImageDiff.init(viewTypes.ONION_SKIN); + expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN); + }); + + it('should default to viewTypes.TWO_UP', () => { + expect(replacedImageDiff.currentView).toEqual(viewTypes.TWO_UP); + }); + }); + }); + + describe('generateImageEls', () => { + beforeEach(() => { + spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + + replacedImageDiff = new ReplacedImageDiff(element, { + canCreateNote: false, + renderCommentBadge: false, + }); + + setupImageFrameEls(); + }); + + it('should set imageEls', () => { + replacedImageDiff.generateImageEls(); + const { imageEls } = replacedImageDiff; + expect(imageEls).toBeDefined(); + expect(imageEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up img')); + expect(imageEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe img')); + expect(imageEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin img')); + }); + }); + + describe('bindEvents', () => { + beforeEach(() => { + spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + replacedImageDiff = new ReplacedImageDiff(element); + + setupViewModesEls(); + }); + + it('should call super.bindEvents', () => { + replacedImageDiff.bindEvents(); + expect(ImageDiff.prototype.bindEvents).toHaveBeenCalled(); + }); + + it('should register click eventlistener to 2-up view mode', (done) => { + spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => { + expect(viewMode).toEqual(viewTypes.TWO_UP); + done(); + }); + + replacedImageDiff.bindEvents(); + replacedImageDiff.viewModesEls[viewTypes.TWO_UP].click(); + }); + + it('should register click eventlistener to swipe view mode', (done) => { + spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => { + expect(viewMode).toEqual(viewTypes.SWIPE); + done(); + }); + + replacedImageDiff.bindEvents(); + replacedImageDiff.viewModesEls[viewTypes.SWIPE].click(); + }); + + it('should register click eventlistener to onion skin view mode', (done) => { + spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => { + expect(viewMode).toEqual(viewTypes.SWIPE); + done(); + }); + + replacedImageDiff.bindEvents(); + replacedImageDiff.viewModesEls[viewTypes.SWIPE].click(); + }); + }); + + describe('getters', () => { + describe('imageEl', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + replacedImageDiff.currentView = viewTypes.TWO_UP; + setupImageEls(); + }); + + it('should return imageEl based on currentView', () => { + expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.two-up img')); + + replacedImageDiff.currentView = viewTypes.SWIPE; + expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.swipe img')); + }); + }); + + describe('imageFrameEl', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + replacedImageDiff.currentView = viewTypes.TWO_UP; + setupImageFrameEls(); + }); + + it('should return imageFrameEl based on currentView', () => { + expect(replacedImageDiff.imageFrameEl).toEqual(element.querySelector('.two-up .js-image-frame')); + + replacedImageDiff.currentView = viewTypes.ONION_SKIN; + expect(replacedImageDiff.imageFrameEl).toEqual(element.querySelector('.onion-skin .js-image-frame')); + }); + }); + }); + + describe('changeView', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + spyOn(imageDiffHelper, 'removeCommentIndicator').and.returnValue({ + removed: false, + }); + setupImageFrameEls(); + }); + + describe('invalid viewType', () => { + beforeEach(() => { + replacedImageDiff.changeView('some-view-name'); + }); + + it('should not call removeCommentIndicator', () => { + expect(imageDiffHelper.removeCommentIndicator).not.toHaveBeenCalled(); + }); + }); + + describe('valid viewType', () => { + beforeEach(() => { + jasmine.clock().install(); + spyOn(ReplacedImageDiff.prototype, 'renderNewView').and.callFake(() => {}); + replacedImageDiff.changeView(viewTypes.ONION_SKIN); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + + it('should call removeCommentIndicator', () => { + expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled(); + }); + + it('should update currentView to newView', () => { + expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN); + }); + + it('should clear imageBadges', () => { + expect(replacedImageDiff.imageBadges.length).toEqual(0); + }); + + it('should call renderNewView', () => { + jasmine.clock().tick(251); + expect(replacedImageDiff.renderNewView).toHaveBeenCalled(); + }); + }); + }); + + describe('renderNewView', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + }); + + it('should call renderBadges', () => { + spyOn(ReplacedImageDiff.prototype, 'renderBadges').and.callFake(() => {}); + + replacedImageDiff.renderNewView({ + removed: false, + }); + + expect(replacedImageDiff.renderBadges).toHaveBeenCalled(); + }); + + describe('removeIndicator', () => { + const indicator = { + removed: true, + x: 0, + y: 1, + image: { + width: 50, + height: 100, + }, + }; + + beforeEach(() => { + setupImageEls(); + setupImageFrameEls(); + }); + + it('should pass showCommentIndicator normalized indicator values', (done) => { + spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {}); + spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.callFake((imageEl, meta) => { + expect(meta.x).toEqual(indicator.x); + expect(meta.y).toEqual(indicator.y); + expect(meta.width).toEqual(indicator.image.width); + expect(meta.height).toEqual(indicator.image.height); + done(); + }); + replacedImageDiff.renderNewView(indicator); + }); + + it('should call showCommentIndicator', (done) => { + const normalized = { + normalized: true, + }; + spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(normalized); + spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake((imageFrameEl, normalizedIndicator) => { + expect(normalizedIndicator).toEqual(normalized); + done(); + }); + replacedImageDiff.renderNewView(indicator); + }); + }); + }); +}); diff --git a/spec/javascripts/image_diff/view_types_spec.js b/spec/javascripts/image_diff/view_types_spec.js new file mode 100644 index 00000000000..e9639f46497 --- /dev/null +++ b/spec/javascripts/image_diff/view_types_spec.js @@ -0,0 +1,24 @@ +import { viewTypes, isValidViewType } from '~/image_diff/view_types'; + +describe('viewTypes', () => { + describe('isValidViewType', () => { + it('should return true for TWO_UP', () => { + expect(isValidViewType(viewTypes.TWO_UP)).toEqual(true); + }); + + it('should return true for SWIPE', () => { + expect(isValidViewType(viewTypes.SWIPE)).toEqual(true); + }); + + it('should return true for ONION_SKIN', () => { + expect(isValidViewType(viewTypes.ONION_SKIN)).toEqual(true); + }); + + it('should return false for non view types', () => { + expect(isValidViewType('some-view-type')).toEqual(false); + expect(isValidViewType(null)).toEqual(false); + expect(isValidViewType(undefined)).toEqual(false); + expect(isValidViewType('')).toEqual(false); + }); + }); +}); diff --git a/spec/javascripts/issuable_context_spec.js b/spec/javascripts/issuable_context_spec.js new file mode 100644 index 00000000000..bcb2b7b24a0 --- /dev/null +++ b/spec/javascripts/issuable_context_spec.js @@ -0,0 +1,34 @@ +/* global IssuableContext */ +import '~/issuable_context'; +import $ from 'jquery'; + +describe('IssuableContext', () => { + describe('toggleHiddenParticipants', () => { + const event = jasmine.createSpyObj('event', ['preventDefault']); + + beforeEach(() => { + spyOn($.fn, 'data').and.returnValue('data'); + spyOn($.fn, 'text').and.returnValue('data'); + }); + + afterEach(() => { + gl.lazyLoader = undefined; + }); + + it('calls loadCheck if lazyLoader is set', () => { + gl.lazyLoader = jasmine.createSpyObj('lazyLoader', ['loadCheck']); + + IssuableContext.prototype.toggleHiddenParticipants(event); + + expect(gl.lazyLoader.loadCheck).toHaveBeenCalled(); + }); + + it('does not throw if lazyLoader is not defined', () => { + gl.lazyLoader = undefined; + + const toggle = IssuableContext.prototype.toggleHiddenParticipants.bind(null, event); + + expect(toggle).not.toThrow(); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 39065814bc2..583a3a74d77 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -42,7 +42,6 @@ describe('Issuable output', () => { initialDescriptionText: '', markdownPreviewPath: '/', markdownDocsPath: '/', - isConfidential: false, projectNamespace: '/', projectPath: '/', }, @@ -157,30 +156,6 @@ describe('Issuable output', () => { }); }); - it('reloads the page if the confidential status has changed', (done) => { - spyOn(gl.utils, 'visitUrl'); - spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { - resolve({ - json() { - return { - confidential: true, - web_url: location.pathname, - }; - }, - }); - })); - - vm.updateIssuable(); - - setTimeout(() => { - expect( - gl.utils.visitUrl, - ).toHaveBeenCalledWith(location.pathname); - - done(); - }); - }); - it('correctly updates issuable data', (done) => { spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve(); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index a6ad250bd86..f86f2f260c3 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -1,398 +1,481 @@ /* eslint-disable promise/catch-or-return */ -import '~/lib/utils/common_utils'; +import * as commonUtils from '~/lib/utils/common_utils'; -(() => { - describe('common_utils', () => { - describe('gl.utils.parseUrl', () => { - it('returns an anchor tag with url', () => { - expect(gl.utils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url'); - }); - it('url is escaped', () => { - // IE11 will return a relative pathname while other browsers will return a full pathname. - // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor - // element will create an absolute url relative to the current execution context. - // The JavaScript test suite is executed at '/' which will lead to an absolute url - // starting with '/'. - expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22'); - }); +describe('common_utils', () => { + describe('parseUrl', () => { + it('returns an anchor tag with url', () => { + expect(commonUtils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url'); }); + it('url is escaped', () => { + // IE11 will return a relative pathname while other browsers will return a full pathname. + // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor + // element will create an absolute url relative to the current execution context. + // The JavaScript test suite is executed at '/' which will lead to an absolute url + // starting with '/'. + expect(commonUtils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22'); + }); + }); - describe('gl.utils.parseUrlPathname', () => { - beforeEach(() => { - spyOn(gl.utils, 'parseUrl').and.callFake(url => ({ - pathname: url, - })); - }); - it('returns an absolute url when given an absolute url', () => { - expect(gl.utils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url'); - }); - it('returns an absolute url when given a relative url', () => { - expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); - }); + describe('parseUrlPathname', () => { + it('returns an absolute url when given an absolute url', () => { + expect(commonUtils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url'); }); - describe('gl.utils.getUrlParamsArray', () => { - it('should return params array', () => { - expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true); - }); + it('returns an absolute url when given a relative url', () => { + expect(commonUtils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); + }); + }); - it('should remove the question mark from the search params', () => { - const paramsArray = gl.utils.getUrlParamsArray(); - expect(paramsArray[0][0] !== '?').toBe(true); - }); + describe('getUrlParamsArray', () => { + it('should return params array', () => { + expect(commonUtils.getUrlParamsArray() instanceof Array).toBe(true); + }); + + it('should remove the question mark from the search params', () => { + const paramsArray = commonUtils.getUrlParamsArray(); + expect(paramsArray[0][0] !== '?').toBe(true); + }); - it('should decode params', () => { - history.pushState('', '', '?label_name%5B%5D=test'); + it('should decode params', () => { + history.pushState('', '', '?label_name%5B%5D=test'); - expect( - gl.utils.getUrlParamsArray()[0], - ).toBe('label_name[]=test'); + expect( + commonUtils.getUrlParamsArray()[0], + ).toBe('label_name[]=test'); - history.pushState('', '', '?'); - }); + history.pushState('', '', '?'); }); + }); - describe('gl.utils.handleLocationHash', () => { - beforeEach(() => { - spyOn(window.document, 'getElementById').and.callThrough(); - }); + describe('handleLocationHash', () => { + beforeEach(() => { + spyOn(window.document, 'getElementById').and.callThrough(); + }); - afterEach(() => { - window.history.pushState({}, null, ''); - }); + afterEach(() => { + window.history.pushState({}, null, ''); + }); - function expectGetElementIdToHaveBeenCalledWith(elementId) { - expect(window.document.getElementById).toHaveBeenCalledWith(elementId); - } + function expectGetElementIdToHaveBeenCalledWith(elementId) { + expect(window.document.getElementById).toHaveBeenCalledWith(elementId); + } - it('decodes hash parameter', () => { - window.history.pushState({}, null, '#random-hash'); - gl.utils.handleLocationHash(); + it('decodes hash parameter', () => { + window.history.pushState({}, null, '#random-hash'); + commonUtils.handleLocationHash(); - expectGetElementIdToHaveBeenCalledWith('random-hash'); - expectGetElementIdToHaveBeenCalledWith('user-content-random-hash'); - }); + expectGetElementIdToHaveBeenCalledWith('random-hash'); + expectGetElementIdToHaveBeenCalledWith('user-content-random-hash'); + }); - it('decodes cyrillic hash parameter', () => { - window.history.pushState({}, null, '#definição'); - gl.utils.handleLocationHash(); + it('decodes cyrillic hash parameter', () => { + window.history.pushState({}, null, '#definição'); + commonUtils.handleLocationHash(); - expectGetElementIdToHaveBeenCalledWith('definição'); - expectGetElementIdToHaveBeenCalledWith('user-content-definição'); - }); + expectGetElementIdToHaveBeenCalledWith('definição'); + expectGetElementIdToHaveBeenCalledWith('user-content-definição'); + }); - it('decodes encoded cyrillic hash parameter', () => { - window.history.pushState({}, null, '#defini%C3%A7%C3%A3o'); - gl.utils.handleLocationHash(); + it('decodes encoded cyrillic hash parameter', () => { + window.history.pushState({}, null, '#defini%C3%A7%C3%A3o'); + commonUtils.handleLocationHash(); - expectGetElementIdToHaveBeenCalledWith('definição'); - expectGetElementIdToHaveBeenCalledWith('user-content-definição'); - }); + expectGetElementIdToHaveBeenCalledWith('definição'); + expectGetElementIdToHaveBeenCalledWith('user-content-definição'); }); - describe('gl.utils.setParamInURL', () => { - afterEach(() => { - window.history.pushState({}, null, ''); - }); + it('scrolls element into view', () => { + document.body.innerHTML += ` + <div id="parent"> + <div style="height: 2000px;"></div> + <div id="test" style="height: 2000px;"></div> + </div> + `; - it('should return the parameter', () => { - window.history.replaceState({}, null, ''); + window.history.pushState({}, null, '#test'); + commonUtils.handleLocationHash(); - expect(gl.utils.setParamInURL('page', 156)).toBe('?page=156'); - expect(gl.utils.setParamInURL('page', '156')).toBe('?page=156'); - }); + expectGetElementIdToHaveBeenCalledWith('test'); + expect(window.scrollY).toBe(document.getElementById('test').offsetTop); - it('should update the existing parameter when its a number', () => { - window.history.pushState({}, null, '?page=15'); + document.getElementById('parent').remove(); + }); - expect(gl.utils.setParamInURL('page', 16)).toBe('?page=16'); - expect(gl.utils.setParamInURL('page', '16')).toBe('?page=16'); - expect(gl.utils.setParamInURL('page', true)).toBe('?page=true'); - }); + it('scrolls user content element into view', () => { + document.body.innerHTML += ` + <div id="parent"> + <div style="height: 2000px;"></div> + <div id="user-content-test" style="height: 2000px;"></div> + </div> + `; - it('should update the existing parameter when its a string', () => { - window.history.pushState({}, null, '?scope=all'); + window.history.pushState({}, null, '#test'); + commonUtils.handleLocationHash(); - expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished'); - }); + expectGetElementIdToHaveBeenCalledWith('test'); + expectGetElementIdToHaveBeenCalledWith('user-content-test'); + expect(window.scrollY).toBe(document.getElementById('user-content-test').offsetTop); - it('should update the existing parameter when more than one parameter exists', () => { - window.history.pushState({}, null, '?scope=all&page=15'); + document.getElementById('parent').remove(); + }); - expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15'); - }); + it('scrolls to element with offset from navbar', () => { + spyOn(window, 'scrollBy').and.callThrough(); + document.body.innerHTML += ` + <div id="parent"> + <div class="navbar-gitlab" style="position: fixed; top: 0; height: 50px;"></div> + <div style="height: 2000px; margin-top: 50px;"></div> + <div id="user-content-test" style="height: 2000px;"></div> + </div> + `; + + window.history.pushState({}, null, '#test'); + commonUtils.handleLocationHash(); + + expectGetElementIdToHaveBeenCalledWith('test'); + expectGetElementIdToHaveBeenCalledWith('user-content-test'); + expect(window.scrollY).toBe(document.getElementById('user-content-test').offsetTop - 50); + expect(window.scrollBy).toHaveBeenCalledWith(0, -50); + + document.getElementById('parent').remove(); + }); + }); + + describe('setParamInURL', () => { + afterEach(() => { + window.history.pushState({}, null, ''); + }); - it('should add a new parameter to the end of the existing ones', () => { - window.history.pushState({}, null, '?scope=all'); + it('should return the parameter', () => { + window.history.replaceState({}, null, ''); - expect(gl.utils.setParamInURL('page', 16)).toBe('?scope=all&page=16'); - expect(gl.utils.setParamInURL('page', '16')).toBe('?scope=all&page=16'); - expect(gl.utils.setParamInURL('page', true)).toBe('?scope=all&page=true'); - }); + expect(commonUtils.setParamInURL('page', 156)).toBe('?page=156'); + expect(commonUtils.setParamInURL('page', '156')).toBe('?page=156'); }); - describe('gl.utils.getParameterByName', () => { - beforeEach(() => { - window.history.pushState({}, null, '?scope=all&p=2'); - }); + it('should update the existing parameter when its a number', () => { + window.history.pushState({}, null, '?page=15'); - afterEach(() => { - window.history.replaceState({}, null, null); - }); + expect(commonUtils.setParamInURL('page', 16)).toBe('?page=16'); + expect(commonUtils.setParamInURL('page', '16')).toBe('?page=16'); + expect(commonUtils.setParamInURL('page', true)).toBe('?page=true'); + }); - it('should return valid parameter', () => { - const value = gl.utils.getParameterByName('scope'); - expect(gl.utils.getParameterByName('p')).toEqual('2'); - expect(value).toBe('all'); - }); + it('should update the existing parameter when its a string', () => { + window.history.pushState({}, null, '?scope=all'); - it('should return invalid parameter', () => { - const value = gl.utils.getParameterByName('fakeParameter'); - expect(value).toBe(null); - }); + expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished'); + }); - it('should return valid paramentes if URL is provided', () => { - let value = gl.utils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar'); - expect(value).toBe('bar'); + it('should update the existing parameter when more than one parameter exists', () => { + window.history.pushState({}, null, '?scope=all&page=15'); - value = gl.utils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu'); - expect(value).toBe('canchu'); - }); + expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15'); }); - describe('gl.utils.normalizedHeaders', () => { - it('should upperCase all the header keys to keep them consistent', () => { - const apiHeaders = { - 'X-Something-Workhorse': { workhorse: 'ok' }, - 'x-something-nginx': { nginx: 'ok' }, - }; + it('should add a new parameter to the end of the existing ones', () => { + window.history.pushState({}, null, '?scope=all'); - const normalized = gl.utils.normalizeHeaders(apiHeaders); + expect(commonUtils.setParamInURL('page', 16)).toBe('?scope=all&page=16'); + expect(commonUtils.setParamInURL('page', '16')).toBe('?scope=all&page=16'); + expect(commonUtils.setParamInURL('page', true)).toBe('?scope=all&page=true'); + }); + }); - const WORKHORSE = 'X-SOMETHING-WORKHORSE'; - const NGINX = 'X-SOMETHING-NGINX'; + describe('getParameterByName', () => { + beforeEach(() => { + window.history.pushState({}, null, '?scope=all&p=2'); + }); - expect(normalized[WORKHORSE].workhorse).toBe('ok'); - expect(normalized[NGINX].nginx).toBe('ok'); - }); + afterEach(() => { + window.history.replaceState({}, null, null); }); - describe('gl.utils.normalizeCRLFHeaders', () => { - beforeEach(function () { - this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + it('should return valid parameter', () => { + const value = commonUtils.getParameterByName('scope'); + expect(commonUtils.getParameterByName('p')).toEqual('2'); + expect(value).toBe('all'); + }); - spyOn(String.prototype, 'split').and.callThrough(); - spyOn(gl.utils, 'normalizeHeaders').and.callThrough(); + it('should return invalid parameter', () => { + const value = commonUtils.getParameterByName('fakeParameter'); + expect(value).toBe(null); + }); - this.normalizeCRLFHeaders = gl.utils.normalizeCRLFHeaders(this.CLRFHeaders); - }); + it('should return valid paramentes if URL is provided', () => { + let value = commonUtils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar'); + expect(value).toBe('bar'); - it('should split by newline', function () { - expect(String.prototype.split).toHaveBeenCalledWith('\n'); - }); + value = commonUtils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu'); + expect(value).toBe('canchu'); + }); + }); - it('should split by colon+space for each header', function () { - expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); - }); + describe('normalizedHeaders', () => { + it('should upperCase all the header keys to keep them consistent', () => { + const apiHeaders = { + 'X-Something-Workhorse': { workhorse: 'ok' }, + 'x-something-nginx': { nginx: 'ok' }, + }; - it('should call gl.utils.normalizeHeaders with a parsed headers object', function () { - expect(gl.utils.normalizeHeaders).toHaveBeenCalledWith(jasmine.any(Object)); - }); + const normalized = commonUtils.normalizeHeaders(apiHeaders); - it('should return a normalized headers object', function () { - expect(this.normalizeCRLFHeaders).toEqual({ - 'A-HEADER': 'a-value', - 'ANOTHER-HEADER': 'ANOTHER-VALUE', - 'LAST-HEADER': 'last-VALUE', - }); - }); + const WORKHORSE = 'X-SOMETHING-WORKHORSE'; + const NGINX = 'X-SOMETHING-NGINX'; + + expect(normalized[WORKHORSE].workhorse).toBe('ok'); + expect(normalized[NGINX].nginx).toBe('ok'); }); + }); - describe('gl.utils.parseIntPagination', () => { - it('should parse to integers all string values and return pagination object', () => { - const pagination = { - 'X-PER-PAGE': 10, - 'X-PAGE': 2, - 'X-TOTAL': 30, - 'X-TOTAL-PAGES': 3, - 'X-NEXT-PAGE': 3, - 'X-PREV-PAGE': 1, - }; - - const expectedPagination = { - perPage: 10, - page: 2, - total: 30, - totalPages: 3, - nextPage: 3, - previousPage: 1, - }; - - expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination); - }); + describe('normalizeCRLFHeaders', () => { + beforeEach(function () { + this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + spyOn(String.prototype, 'split').and.callThrough(); + this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders); + }); + + it('should split by newline', function () { + expect(String.prototype.split).toHaveBeenCalledWith('\n'); }); - describe('gl.utils.isMetaClick', () => { - it('should identify meta click on Windows/Linux', () => { - const e = { - metaKey: false, - ctrlKey: true, - which: 1, - }; + it('should split by colon+space for each header', function () { + expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); + }); - expect(gl.utils.isMetaClick(e)).toBe(true); + it('should return a normalized headers object', function () { + expect(this.normalizeCRLFHeaders).toEqual({ + 'A-HEADER': 'a-value', + 'ANOTHER-HEADER': 'ANOTHER-VALUE', + 'LAST-HEADER': 'last-VALUE', }); + }); + }); + + describe('parseIntPagination', () => { + it('should parse to integers all string values and return pagination object', () => { + const pagination = { + 'X-PER-PAGE': 10, + 'X-PAGE': 2, + 'X-TOTAL': 30, + 'X-TOTAL-PAGES': 3, + 'X-NEXT-PAGE': 3, + 'X-PREV-PAGE': 1, + }; + + const expectedPagination = { + perPage: 10, + page: 2, + total: 30, + totalPages: 3, + nextPage: 3, + previousPage: 1, + }; + + expect(commonUtils.parseIntPagination(pagination)).toEqual(expectedPagination); + }); + }); - it('should identify meta click on macOS', () => { - const e = { - metaKey: true, - ctrlKey: false, - which: 1, - }; + describe('isMetaClick', () => { + it('should identify meta click on Windows/Linux', () => { + const e = { + metaKey: false, + ctrlKey: true, + which: 1, + }; - expect(gl.utils.isMetaClick(e)).toBe(true); - }); + expect(commonUtils.isMetaClick(e)).toBe(true); + }); - it('should identify as meta click on middle-click or Mouse-wheel click', () => { - const e = { - metaKey: false, - ctrlKey: false, - which: 2, - }; + it('should identify meta click on macOS', () => { + const e = { + metaKey: true, + ctrlKey: false, + which: 1, + }; - expect(gl.utils.isMetaClick(e)).toBe(true); + expect(commonUtils.isMetaClick(e)).toBe(true); + }); + + it('should identify as meta click on middle-click or Mouse-wheel click', () => { + const e = { + metaKey: false, + ctrlKey: false, + which: 2, + }; + + expect(commonUtils.isMetaClick(e)).toBe(true); + }); + }); + + describe('convertPermissionToBoolean', () => { + it('should convert a boolean in a string to a boolean', () => { + expect(commonUtils.convertPermissionToBoolean('true')).toEqual(true); + expect(commonUtils.convertPermissionToBoolean('false')).toEqual(false); + }); + }); + + describe('backOff', () => { + beforeEach(() => { + // shortcut our timeouts otherwise these tests will take a long time to finish + const origSetTimeout = window.setTimeout; + spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0)); + }); + + it('solves the promise from the callback', (done) => { + const expectedResponseValue = 'Success!'; + commonUtils.backOff((next, stop) => ( + new Promise((resolve) => { + resolve(expectedResponseValue); + }).then((resp) => { + stop(resp); + }) + )).then((respBackoff) => { + expect(respBackoff).toBe(expectedResponseValue); + done(); }); }); - describe('gl.utils.backOff', () => { - beforeEach(() => { - // shortcut our timeouts otherwise these tests will take a long time to finish - const origSetTimeout = window.setTimeout; - spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0)); + it('catches the rejected promise from the callback ', (done) => { + const errorMessage = 'Mistakes were made!'; + commonUtils.backOff((next, stop) => { + new Promise((resolve, reject) => { + reject(new Error(errorMessage)); + }).then((resp) => { + stop(resp); + }).catch(err => stop(err)); + }).catch((errBackoffResp) => { + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe(errorMessage); + done(); }); + }); - it('solves the promise from the callback', (done) => { - const expectedResponseValue = 'Success!'; - gl.utils.backOff((next, stop) => ( - new Promise((resolve) => { - resolve(expectedResponseValue); - }).then((resp) => { - stop(resp); + it('solves the promise correctly after retrying a third time', (done) => { + let numberOfCalls = 1; + const expectedResponseValue = 'Success!'; + commonUtils.backOff((next, stop) => ( + Promise.resolve(expectedResponseValue) + .then((resp) => { + if (numberOfCalls < 3) { + numberOfCalls += 1; + next(); + } else { + stop(resp); + } }) - )).then((respBackoff) => { - expect(respBackoff).toBe(expectedResponseValue); - done(); - }); + )).then((respBackoff) => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + expect(timeouts).toEqual([2000, 4000]); + expect(respBackoff).toBe(expectedResponseValue); + done(); }); + }); - it('catches the rejected promise from the callback ', (done) => { - const errorMessage = 'Mistakes were made!'; - gl.utils.backOff((next, stop) => { - new Promise((resolve, reject) => { - reject(new Error(errorMessage)); - }).then((resp) => { - stop(resp); - }).catch(err => stop(err)); - }).catch((errBackoffResp) => { + it('rejects the backOff promise after timing out', (done) => { + commonUtils.backOff(next => next(), 64000) + .catch((errBackoffResp) => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe(errorMessage); + expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); done(); }); - }); + }); + }); - it('solves the promise correctly after retrying a third time', (done) => { - let numberOfCalls = 1; - const expectedResponseValue = 'Success!'; - gl.utils.backOff((next, stop) => ( - Promise.resolve(expectedResponseValue) - .then((resp) => { - if (numberOfCalls < 3) { - numberOfCalls += 1; - next(); - } else { - stop(resp); - } - }) - )).then((respBackoff) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000]); - expect(respBackoff).toBe(expectedResponseValue); - done(); - }); - }); + describe('setFavicon', () => { + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('href', 'default/favicon'); + document.body.appendChild(favicon); + }); - it('rejects the backOff promise after timing out', (done) => { - gl.utils.backOff(next => next(), 64000) - .catch((errBackoffResp) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); - expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); - done(); - }); - }); + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); }); + it('should set page favicon to provided favicon', () => { + const faviconPath = '//custom_favicon'; + commonUtils.setFavicon(faviconPath); - describe('gl.utils.setFavicon', () => { - it('should set page favicon to provided favicon', () => { - const faviconPath = '//custom_favicon'; - const fakeLink = { - setAttribute() {}, - }; + expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconPath); + }); + }); - spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); - spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { - expect(attr).toEqual('href'); - expect(val.indexOf(faviconPath) > -1).toBe(true); - }); - gl.utils.setFavicon(faviconPath); - }); + describe('resetFavicon', () => { + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('href', 'default/favicon'); + document.body.appendChild(favicon); }); - describe('gl.utils.resetFavicon', () => { - it('should reset page favicon to tanuki', () => { - const fakeLink = { - setAttribute() {}, - }; + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); - spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); - spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { - expect(attr).toEqual('href'); - expect(val).toMatch(/favicon/); - }); - gl.utils.resetFavicon(); + it('should reset page favicon to tanuki', () => { + commonUtils.resetFavicon(); + expect(document.getElementById('favicon').getAttribute('href')).toEqual('default/favicon'); + }); + }); + + describe('setCiStatusFavicon', () => { + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; + + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + document.body.appendChild(favicon); + }); + + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); + + it('should reset favicon in case of error', () => { + const favicon = document.getElementById('favicon'); + spyOn($, 'ajax').and.callFake(function (options) { + options.error(); + expect(favicon.getAttribute('href')).toEqual('null'); }); + + commonUtils.setCiStatusFavicon(BUILD_URL); }); - describe('gl.utils.setCiStatusFavicon', () => { - it('should set page favicon to CI status favicon based on provided status', () => { - const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; - const FAVICON_PATH = '//icon_status_success'; - const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub(); - const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub(); - spyOn($, 'ajax').and.callFake(function (options) { - options.success({ favicon: FAVICON_PATH }); - expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH); - options.success(); - expect(spyResetFavicon).toHaveBeenCalled(); - options.error(); - expect(spyResetFavicon).toHaveBeenCalled(); - }); + it('should set page favicon to CI status favicon based on provided status', () => { + const FAVICON_PATH = '//icon_status_success'; + const favicon = document.getElementById('favicon'); - gl.utils.setCiStatusFavicon(BUILD_URL); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ favicon: FAVICON_PATH }); + expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); }); + + commonUtils.setCiStatusFavicon(BUILD_URL); }); + }); - describe('gl.utils.ajaxPost', () => { - it('should perform `$.ajax` call and do `POST` request', () => { - const requestURL = '/some/random/api'; - const data = { keyname: 'value' }; - const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {}); + describe('ajaxPost', () => { + it('should perform `$.ajax` call and do `POST` request', () => { + const requestURL = '/some/random/api'; + const data = { keyname: 'value' }; + const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {}); + + commonUtils.ajaxPost(requestURL, data); + expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST'); + }); + + describe('gl.utils.spriteIcon', () => { + beforeEach(() => { + window.gon.sprite_icons = 'icons.svg'; + }); - gl.utils.ajaxPost(requestURL, data); - expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST'); + it('should return the svg for a linked icon', () => { + expect(gl.utils.spriteIcon('test')).toEqual('<svg><use xlink:href="icons.svg#test" /></svg>'); }); }); }); -})(); +}); diff --git a/spec/javascripts/lib/utils/csrf_token_spec.js b/spec/javascripts/lib/utils/csrf_token_spec.js new file mode 100644 index 00000000000..c484213df8e --- /dev/null +++ b/spec/javascripts/lib/utils/csrf_token_spec.js @@ -0,0 +1,49 @@ +import csrf from '~/lib/utils/csrf'; + +describe('csrf', () => { + beforeEach(() => { + this.tokenKey = 'X-CSRF-Token'; + this.token = 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ=='; + }); + + it('returns the correct headerKey', () => { + expect(csrf.headerKey).toBe(this.tokenKey); + }); + + describe('when csrf token is in the DOM', () => { + beforeEach(() => { + setFixtures(` + <meta name="csrf-token" content="${this.token}"> + `); + + csrf.init(); + }); + + it('returns the csrf token', () => { + expect(csrf.token).toBe(this.token); + }); + + it('returns the csrf headers object', () => { + expect(csrf.headers[this.tokenKey]).toBe(this.token); + }); + }); + + describe('when csrf token is not in the DOM', () => { + beforeEach(() => { + setFixtures(` + <meta name="some-other-token"> + `); + + csrf.init(); + }); + + it('returns null for token', () => { + expect(csrf.token).toBeNull(); + }); + + it('returns empty object for headers', () => { + expect(typeof csrf.headers).toBe('object'); + expect(Object.keys(csrf.headers).length).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/javascripts/lib/utils/image_utility_spec.js new file mode 100644 index 00000000000..75addfcc833 --- /dev/null +++ b/spec/javascripts/lib/utils/image_utility_spec.js @@ -0,0 +1,32 @@ +import * as imageUtility from '~/lib/utils/image_utility'; + +describe('imageUtility', () => { + describe('isImageLoaded', () => { + it('should return false when image.complete is false', () => { + const element = { + complete: false, + naturalHeight: 100, + }; + + expect(imageUtility.isImageLoaded(element)).toEqual(false); + }); + + it('should return false when naturalHeight = 0', () => { + const element = { + complete: true, + naturalHeight: 0, + }; + + expect(imageUtility.isImageLoaded(element)).toEqual(false); + }); + + it('should return true when image.complete and naturalHeight != 0', () => { + const element = { + complete: true, + naturalHeight: 100, + }; + + expect(imageUtility.isImageLoaded(element)).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/sticky_spec.js b/spec/javascripts/lib/utils/sticky_spec.js index c3ee3ef9825..b87c836654d 100644 --- a/spec/javascripts/lib/utils/sticky_spec.js +++ b/spec/javascripts/lib/utils/sticky_spec.js @@ -1,52 +1,79 @@ import { isSticky } from '~/lib/utils/sticky'; describe('sticky', () => { - const el = { - offsetTop: 0, - classList: {}, - }; + let el; beforeEach(() => { - el.offsetTop = 0; - el.classList.add = jasmine.createSpy('spy'); - el.classList.remove = jasmine.createSpy('spy'); + document.body.innerHTML += ` + <div class="parent"> + <div id="js-sticky"></div> + </div> + `; + + el = document.getElementById('js-sticky'); }); - describe('classList.remove', () => { - it('does not call classList.remove when stuck', () => { - isSticky(el, 0, 0); + afterEach(() => { + el.parentNode.remove(); + }); + + describe('when stuck', () => { + it('does not remove is-stuck class', () => { + isSticky(el, 0, el.offsetTop); + isSticky(el, 0, el.offsetTop); expect( - el.classList.remove, - ).not.toHaveBeenCalled(); + el.classList.contains('is-stuck'), + ).toBeTruthy(); }); - it('calls classList.remove when not stuck', () => { - el.offsetTop = 10; - isSticky(el, 0, 0); + it('adds is-stuck class', () => { + isSticky(el, 0, el.offsetTop); expect( - el.classList.remove, - ).toHaveBeenCalledWith('is-stuck'); + el.classList.contains('is-stuck'), + ).toBeTruthy(); + }); + + it('inserts placeholder element', () => { + isSticky(el, 0, el.offsetTop, true); + + expect( + document.querySelector('.sticky-placeholder'), + ).not.toBeNull(); }); }); - describe('classList.add', () => { - it('calls classList.add when stuck', () => { + describe('when not stuck', () => { + it('removes is-stuck class', () => { + spyOn(el.classList, 'remove').and.callThrough(); + + isSticky(el, 0, el.offsetTop); isSticky(el, 0, 0); expect( - el.classList.add, + el.classList.remove, ).toHaveBeenCalledWith('is-stuck'); + expect( + el.classList.contains('is-stuck'), + ).toBeFalsy(); }); - it('does not call classList.add when not stuck', () => { - el.offsetTop = 10; + it('does not add is-stuck class', () => { isSticky(el, 0, 0); expect( - el.classList.add, - ).not.toHaveBeenCalled(); + el.classList.contains('is-stuck'), + ).toBeFalsy(); + }); + + it('removes placeholder', () => { + isSticky(el, 0, el.offsetTop, true); + isSticky(el, 0, 0, true); + + expect( + document.querySelector('.sticky-placeholder'), + ).toBeNull(); }); }); }); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index aee274641e8..645664a5219 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -18,19 +18,25 @@ import '~/line_highlighter'; beforeEach(function() { loadFixtures('static/line_highlighter.html.raw'); this["class"] = new LineHighlighter(); - this.css = this["class"].highlightClass; + this.css = this["class"].highlightLineClass; return this.spies = { __setLocationHash__: spyOn(this["class"], '__setLocationHash__').and.callFake(function() {}) }; }); describe('behavior', function() { it('highlights one line given in the URL hash', function() { - new LineHighlighter('#L13'); + new LineHighlighter({ hash: '#L13' }); return expect($('#LC13')).toHaveClass(this.css); }); + it('highlights one line given in the URL hash with given CSS class name', function() { + const hiliter = new LineHighlighter({ hash: '#L13', highlightLineClass: 'hilite' }); + expect(hiliter.highlightLineClass).toBe('hilite'); + expect($('#LC13')).toHaveClass('hilite'); + expect($('#LC13')).not.toHaveClass('hll'); + }); it('highlights a range of lines given in the URL hash', function() { var line, results; - new LineHighlighter('#L5-25'); + new LineHighlighter({ hash: '#L5-25' }); expect($("." + this.css).length).toBe(21); results = []; for (line = 5; line <= 25; line += 1) { @@ -41,7 +47,7 @@ import '~/line_highlighter'; it('scrolls to the first highlighted line on initial load', function() { var spy; spy = spyOn($, 'scrollTo'); - new LineHighlighter('#L5-25'); + new LineHighlighter({ hash: '#L5-25' }); return expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()); }); it('discards click events', function() { @@ -50,10 +56,10 @@ import '~/line_highlighter'; clickLine(13); return expect(spy).toHaveBeenPrevented(); }); - return it('handles garbage input from the hash', function() { + it('handles garbage input from the hash', function() { var func; func = function() { - return new LineHighlighter('#blob-content-holder'); + return new LineHighlighter({ fileHolderSelector: '#blob-content-holder' }); }; return expect(func).not.toThrow(); }); diff --git a/spec/javascripts/locale/sprintf_spec.js b/spec/javascripts/locale/sprintf_spec.js new file mode 100644 index 00000000000..52e903b819f --- /dev/null +++ b/spec/javascripts/locale/sprintf_spec.js @@ -0,0 +1,74 @@ +import sprintf from '~/locale/sprintf'; + +describe('locale', () => { + describe('sprintf', () => { + it('does not modify string without parameters', () => { + const input = 'No parameters'; + + const output = sprintf(input); + + expect(output).toBe(input); + }); + + it('ignores extraneous parameters', () => { + const input = 'No parameters'; + + const output = sprintf(input, { ignore: 'this' }); + + expect(output).toBe(input); + }); + + it('ignores extraneous placeholders', () => { + const input = 'No %{parameters}'; + + const output = sprintf(input); + + expect(output).toBe(input); + }); + + it('replaces parameters', () => { + const input = '%{name} has %{count} parameters'; + const parameters = { + name: 'this', + count: 2, + }; + + const output = sprintf(input, parameters); + + expect(output).toBe('this has 2 parameters'); + }); + + it('replaces multiple occurrences', () => { + const input = 'to %{verb} or not to %{verb}'; + const parameters = { + verb: 'be', + }; + + const output = sprintf(input, parameters); + + expect(output).toBe('to be or not to be'); + }); + + it('escapes parameters', () => { + const input = 'contains %{userContent}'; + const parameters = { + userContent: '<script>alert("malicious!")</script>', + }; + + const output = sprintf(input, parameters); + + expect(output).toBe('contains <script>alert("malicious!")</script>'); + }); + + it('does not escape parameters for escapeParameters = false', () => { + const input = 'contains %{safeContent}'; + const parameters = { + safeContent: '<strong>bold attempt</strong>', + }; + + const output = sprintf(input, parameters, false); + + expect(output).toBe('contains <strong>bold attempt</strong>'); + }); + }); +}); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 6ff42e2378d..3ab901da6b6 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -58,5 +58,44 @@ import IssuablesHelper from '~/helpers/issuables_helper'; expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled(); }); }); + + describe('hideCloseButton', () => { + describe('merge request of another user', () => { + beforeEach(() => { + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + this.el = document.querySelector('.merge-request .issuable-actions'); + const merge = new MergeRequest(); + merge.hideCloseButton(); + }); + + it('hides the dropdown close item and selects the next item', () => { + const closeItem = this.el.querySelector('li.close-item'); + const smallCloseItem = this.el.querySelector('.js-close-item'); + const reportItem = this.el.querySelector('li.report-item'); + + expect(closeItem).toHaveClass('hidden'); + expect(smallCloseItem).toHaveClass('hidden'); + expect(reportItem).toHaveClass('droplab-item-selected'); + expect(reportItem).not.toHaveClass('hidden'); + }); + }); + + describe('merge request of current_user', () => { + beforeEach(() => { + loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); + this.el = document.querySelector('.merge-request .issuable-actions'); + const merge = new MergeRequest(); + merge.hideCloseButton(); + }); + + it('hides the close button', () => { + const closeButton = this.el.querySelector('.btn-close'); + const smallCloseItem = this.el.querySelector('.js-close-item'); + + expect(closeButton).toHaveClass('hidden'); + expect(smallCloseItem).toHaveClass('hidden'); + }); + }); + }); }); }).call(window); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 8830a2d29e5..ccdbfcba692 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -78,8 +78,9 @@ import 'vendor/jquery.scrollTo'; }); describe('meta click', () => { + let metakeyEvent; beforeEach(function () { - spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); }); it('opens page when commits link is clicked', function () { @@ -89,7 +90,7 @@ import 'vendor/jquery.scrollTo'; }); this.class.bindEvents(); - document.querySelector('.merge-request-tabs .commits-tab a').click(); + $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent); }); it('opens page when commits badge is clicked', function () { @@ -99,7 +100,7 @@ import 'vendor/jquery.scrollTo'; }); this.class.bindEvents(); - document.querySelector('.merge-request-tabs .commits-tab a .badge').click(); + $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent); }); }); @@ -415,5 +416,28 @@ import 'vendor/jquery.scrollTo'; }); }); }); + + describe('expandViewContainer', function () { + beforeEach(() => { + $('body').append('<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>'); + }); + + afterEach(() => { + $('.content-wrapper').remove(); + }); + + it('removes container-limited from containers', function () { + this.class.expandViewContainer(); + + expect($('.content-wrapper')).not.toContainElement('.container-limited'); + }); + + it('does remove container-limited from breadcrumbs', function () { + $('.container-limited').addClass('breadcrumbs'); + this.class.expandViewContainer(); + + expect($('.content-wrapper')).toContainElement('.container-limited'); + }); + }); }); }).call(window); diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js index e8f7042e131..3319eeb3f31 100644 --- a/spec/javascripts/monitoring/dashboard_state_spec.js +++ b/spec/javascripts/monitoring/dashboard_state_spec.js @@ -21,6 +21,9 @@ describe('EmptyState', () => { selectedState: 'gettingStarted', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.currentState).toBe(component.states.gettingStarted); @@ -31,6 +34,9 @@ describe('EmptyState', () => { selectedState: 'gettingStarted', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.buttonPath).toEqual(statePaths.settingsPath); @@ -42,6 +48,9 @@ describe('EmptyState', () => { selectedState: 'loading', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.buttonPath).toEqual(statePaths.documentationPath); @@ -53,6 +62,9 @@ describe('EmptyState', () => { selectedState: 'unableToConnect', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.showButtonDescription).toEqual(true); @@ -63,6 +75,9 @@ describe('EmptyState', () => { selectedState: 'loading', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.showButtonDescription).toEqual(false); @@ -74,6 +89,9 @@ describe('EmptyState', () => { selectedState: 'gettingStarted', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.$el.querySelector('svg')).toBeDefined(); @@ -87,6 +105,9 @@ describe('EmptyState', () => { selectedState: 'loading', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.$el.querySelector('svg')).toBeDefined(); @@ -100,6 +121,9 @@ describe('EmptyState', () => { selectedState: 'unableToConnect', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.$el.querySelector('svg')).toBeDefined(); diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js index c2ff38ffab9..dea42d755d4 100644 --- a/spec/javascripts/monitoring/graph/deployment_spec.js +++ b/spec/javascripts/monitoring/graph/deployment_spec.js @@ -21,6 +21,7 @@ describe('MonitoringDeployment', () => { const component = createComponent({ showDeployInfo: false, deploymentData: reducedDeploymentData, + graphWidth: 440, graphHeight: 300, graphHeightOffset: 120, }); @@ -36,6 +37,7 @@ describe('MonitoringDeployment', () => { showDeployInfo: false, deploymentData: reducedDeploymentData, graphHeight: 300, + graphWidth: 440, graphHeightOffset: 120, }); @@ -49,6 +51,7 @@ describe('MonitoringDeployment', () => { showDeployInfo: false, deploymentData: reducedDeploymentData, graphHeight: 300, + graphWidth: 440, graphHeightOffset: 120, }); @@ -62,6 +65,7 @@ describe('MonitoringDeployment', () => { showDeployInfo: false, deploymentData: reducedDeploymentData, graphHeight: 300, + graphWidth: 440, graphHeightOffset: 120, }); @@ -75,6 +79,7 @@ describe('MonitoringDeployment', () => { const component = createComponent({ showDeployInfo: true, deploymentData: reducedDeploymentData, + graphWidth: 440, graphHeight: 300, graphHeightOffset: 120, }); @@ -82,12 +87,29 @@ describe('MonitoringDeployment', () => { expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull(); }); + it('positions the flag to the left when the xPos is too far right', () => { + reducedDeploymentData[0].showDeploymentFlag = false; + reducedDeploymentData[0].xPos = 250; + const component = createComponent({ + showDeployInfo: true, + deploymentData: reducedDeploymentData, + graphWidth: 440, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect( + component.positionFlag(reducedDeploymentData[0]), + ).toBeLessThan(0); + }); + it('shows the deployment flag', () => { reducedDeploymentData[0].showDeploymentFlag = true; const component = createComponent({ showDeployInfo: true, deploymentData: reducedDeploymentData, graphHeight: 300, + graphWidth: 440, graphHeightOffset: 120, }); @@ -102,6 +124,7 @@ describe('MonitoringDeployment', () => { showDeployInfo: true, deploymentData: reducedDeploymentData, graphHeight: 300, + graphWidth: 440, graphHeightOffset: 120, }); @@ -115,6 +138,7 @@ describe('MonitoringDeployment', () => { showDeployInfo: true, deploymentData: reducedDeploymentData, graphHeight: 300, + graphWidth: 440, graphHeightOffset: 120, }); @@ -127,6 +151,7 @@ describe('MonitoringDeployment', () => { showDeployInfo: true, deploymentData: reducedDeploymentData, graphHeight: 300, + graphWidth: 440, graphHeightOffset: 120, }); diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index 14794cbfd50..8ee1171419d 100644 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -14,19 +14,22 @@ function getCoordinate(component, selector, coordinate) { return parseInt(coordinateVal, 10); } +const defaultValuesComponent = { + currentXCoordinate: 200, + currentYCoordinate: 100, + currentFlagPosition: 100, + currentData: { + time: new Date('2017-06-04T18:17:33.501Z'), + value: '1.49609375', + }, + graphHeight: 300, + graphHeightOffset: 120, + showFlagContent: true, +}; + describe('GraphFlag', () => { it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => { - const component = createComponent({ - currentXCoordinate: 200, - currentYCoordinate: 100, - currentFlagPosition: 100, - currentData: { - time: new Date('2017-06-04T18:17:33.501Z'), - value: '1.49609375', - }, - graphHeight: 300, - graphHeightOffset: 120, - }); + const component = createComponent(defaultValuesComponent); expect(getCoordinate(component, '.selected-metric-line', 'x1')) .toEqual(component.currentXCoordinate); @@ -35,17 +38,7 @@ describe('GraphFlag', () => { }); it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => { - const component = createComponent({ - currentXCoordinate: 200, - currentYCoordinate: 100, - currentFlagPosition: 100, - currentData: { - time: new Date('2017-06-04T18:17:33.501Z'), - value: '1.49609375', - }, - graphHeight: 300, - graphHeightOffset: 120, - }); + const component = createComponent(defaultValuesComponent); const svg = component.$el.querySelector('.rect-text-metric'); expect(svg.tagName).toEqual('svg'); @@ -54,17 +47,7 @@ describe('GraphFlag', () => { describe('Computed props', () => { it('calculatedHeight', () => { - const component = createComponent({ - currentXCoordinate: 200, - currentYCoordinate: 100, - currentFlagPosition: 100, - currentData: { - time: new Date('2017-06-04T18:17:33.501Z'), - value: '1.49609375', - }, - graphHeight: 300, - graphHeightOffset: 120, - }); + const component = createComponent(defaultValuesComponent); expect(component.calculatedHeight).toEqual(180); }); diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js index da2fbd26e23..2571b7ef869 100644 --- a/spec/javascripts/monitoring/graph/legend_spec.js +++ b/spec/javascripts/monitoring/graph/legend_spec.js @@ -28,7 +28,7 @@ const defaultValuesComponent = { currentDataIndex: 0, }; -const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight, defaultValuesComponent.graphHeightOffset); @@ -89,13 +89,12 @@ describe('GraphLegend', () => { expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); }); - it('contains text to signal the usage, title and time', () => { + it('contains text to signal the usage, title and time with multiple time series', () => { const component = createComponent(defaultValuesComponent); const titles = component.$el.querySelectorAll('.legend-metric-title'); - expect(getTextFromNode(component, '.legend-metric-title').indexOf(component.legendTitle)).not.toEqual(-1); - expect(titles[0].textContent.indexOf('Title')).not.toEqual(-1); - expect(titles[1].textContent.indexOf('Series')).not.toEqual(-1); + expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1); + expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1); expect(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel); }); diff --git a/spec/javascripts/monitoring/monitoring_paths_spec.js b/spec/javascripts/monitoring/graph_path_spec.js index d39db945e17..81825a3ae87 100644 --- a/spec/javascripts/monitoring/monitoring_paths_spec.js +++ b/spec/javascripts/monitoring/graph_path_spec.js @@ -1,10 +1,10 @@ import Vue from 'vue'; -import MonitoringPaths from '~/monitoring/components/monitoring_paths.vue'; +import GraphPath from '~/monitoring/components/graph/path.vue'; import createTimeSeries from '~/monitoring/utils/multiple_time_series'; import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data'; const createComponent = (propsData) => { - const Component = Vue.extend(MonitoringPaths); + const Component = Vue.extend(GraphPath); return new Component({ propsData, @@ -13,22 +13,23 @@ const createComponent = (propsData) => { const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120); +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120); +const firstTimeSeries = timeSeries[0]; describe('Monitoring Paths', () => { it('renders two paths to represent a line and the area underneath it', () => { const component = createComponent({ - generatedLinePath: timeSeries[0].linePath, - generatedAreaPath: timeSeries[0].areaPath, - lineColor: '#ccc', - areaColor: '#fff', + generatedLinePath: firstTimeSeries.linePath, + generatedAreaPath: firstTimeSeries.areaPath, + lineColor: firstTimeSeries.lineColor, + areaColor: firstTimeSeries.areaColor, }); const metricArea = component.$el.querySelector('.metric-area'); const metricLine = component.$el.querySelector('.metric-line'); - expect(metricArea.getAttribute('fill')).toBe('#fff'); - expect(metricArea.getAttribute('d')).toBe(timeSeries[0].areaPath); - expect(metricLine.getAttribute('stroke')).toBe('#ccc'); - expect(metricLine.getAttribute('d')).toBe(timeSeries[0].linePath); + expect(metricArea.getAttribute('fill')).toBe('#8fbce8'); + expect(metricArea.getAttribute('d')).toBe(firstTimeSeries.areaPath); + expect(metricLine.getAttribute('stroke')).toBe('#1f78d1'); + expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath); }); }); diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js index 7d8b0744af1..fd79abe241a 100644 --- a/spec/javascripts/monitoring/graph_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -44,7 +44,7 @@ describe('Graph', () => { .not.toEqual(-1); }); - it('outterViewBox gets a width and height property based on the DOM size of the element', () => { + it('outerViewBox gets a width and height property based on the DOM size of the element', () => { const component = createComponent({ graphData: convertedMetrics[1], classType: 'col-md-6', @@ -52,8 +52,8 @@ describe('Graph', () => { deploymentData, }); - const viewBoxArray = component.outterViewBox.split(' '); - expect(typeof component.outterViewBox).toEqual('string'); + const viewBoxArray = component.outerViewBox.split(' '); + expect(typeof component.outerViewBox).toEqual('string'); expect(viewBoxArray[2]).toEqual(component.graphWidth.toString()); expect(viewBoxArray[3]).toEqual(component.graphHeight.toString()); }); @@ -86,4 +86,22 @@ describe('Graph', () => { expect(component.yAxisLabel).toEqual(component.graphData.y_label); expect(component.legendTitle).toEqual(component.graphData.queries[0].label); }); + + it('sets the currentData object based on the hovered data index', () => { + const component = createComponent({ + graphData: convertedMetrics[1], + classType: 'col-md-6', + updateAspectRatio: false, + deploymentData, + graphIdentifier: 0, + hoverData: { + hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'), + currentDeployXPos: null, + }, + }); + + component.positionFlag(); + expect(component.currentData).toBe(component.timeSeries[0].values[10]); + expect(component.currentDataIndex).toEqual(10); + }); }); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 3d399f2bb95..7ceab657464 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -6346,7 +6346,13 @@ export const singleRowMetricsMultipleSeries = [ } ] }, - ] + ], + 'when': [ + { + 'value': 'hundred(s)', + 'color': 'green', + }, + ], } ] }, diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js index 3daf6bf82df..7e44a9ade9e 100644 --- a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js +++ b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js @@ -2,16 +2,17 @@ import createTimeSeries from '~/monitoring/utils/multiple_time_series'; import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data'; const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120); +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120); +const firstTimeSeries = timeSeries[0]; describe('Multiple time series', () => { it('createTimeSeries returned array contains an object for each element', () => { - expect(typeof timeSeries[0].linePath).toEqual('string'); - expect(typeof timeSeries[0].areaPath).toEqual('string'); - expect(typeof timeSeries[0].timeSeriesScaleX).toEqual('function'); - expect(typeof timeSeries[0].areaColor).toEqual('string'); - expect(typeof timeSeries[0].lineColor).toEqual('string'); - expect(timeSeries[0].values instanceof Array).toEqual(true); + expect(typeof firstTimeSeries.linePath).toEqual('string'); + expect(typeof firstTimeSeries.areaPath).toEqual('string'); + expect(typeof firstTimeSeries.timeSeriesScaleX).toEqual('function'); + expect(typeof firstTimeSeries.areaColor).toEqual('string'); + expect(typeof firstTimeSeries.lineColor).toEqual('string'); + expect(firstTimeSeries.values instanceof Array).toEqual(true); }); it('createTimeSeries returns an array', () => { diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js index cca5ec887a3..3f659af5c3b 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import autosize from 'vendor/autosize'; import store from '~/notes/stores'; import issueCommentForm from '~/notes/components/issue_comment_form.vue'; import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data'; @@ -32,6 +33,30 @@ describe('issue_comment_form component', () => { expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); }); + describe('handleSave', () => { + it('should request to save note when note is entered', () => { + vm.note = 'hello world'; + spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); + spyOn(vm, 'resizeTextarea'); + spyOn(vm, 'stopPolling'); + + vm.handleSave(); + expect(vm.isSubmitting).toEqual(true); + expect(vm.note).toEqual(''); + expect(vm.saveNote).toHaveBeenCalled(); + expect(vm.stopPolling).toHaveBeenCalled(); + expect(vm.resizeTextarea).toHaveBeenCalled(); + }); + + it('should toggle issue state when no note', () => { + spyOn(vm, 'toggleIssueState'); + + vm.handleSave(); + + expect(vm.toggleIssueState).toHaveBeenCalled(); + }); + }); + describe('textarea', () => { it('should render textarea with placeholder', () => { expect( @@ -39,6 +64,22 @@ describe('issue_comment_form component', () => { ).toEqual('Write a comment or drag your files here...'); }); + it('should make textarea disabled while requesting', (done) => { + const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button')); + vm.note = 'hello world'; + spyOn(vm, 'stopPolling'); + spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); + + vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton. + $submitButton.trigger('click'); + + vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea. + expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy(); + done(); + }); + }); + }); + it('should support quick actions', () => { expect( vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'), @@ -55,6 +96,19 @@ describe('issue_comment_form component', () => { expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); }); + it('should resize textarea after note discarded', (done) => { + spyOn(autosize, 'update'); + spyOn(vm, 'discard').and.callThrough(); + + vm.note = 'foo'; + vm.discard(); + + Vue.nextTick(() => { + expect(autosize.update).toHaveBeenCalled(); + done(); + }); + }); + describe('edit mode', () => { it('should enter edit mode when arrow up is pressed', () => { spyOn(vm, 'editCurrentUserLastNote').and.callThrough(); diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 72d362acb2f..3d1ca870ca4 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,6 +1,5 @@ - import * as actions from '~/notes/stores/actions'; -import testAction from './helpers'; +import testAction from '../../helpers/vuex_action_helper'; import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; describe('Actions Notes Store', () => { diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index a38f29c1e39..1e22e03e178 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -3,19 +3,31 @@ import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, indiv describe('Mutation Notes Store', () => { describe('ADD_NEW_NOTE', () => { - it('should add a new note to an array of notes', () => { - const state = { notes: [] }; + let state; + let noteData; + + beforeEach(() => { + state = { notes: [] }; + noteData = { + expanded: true, + id: note.discussion_id, + individual_note: true, + notes: [note], + reply_id: note.discussion_id, + }; mutations.ADD_NEW_NOTE(state, note); + }); + it('should add a new note to an array of notes', () => { expect(state).toEqual({ - notes: [{ - expanded: true, - id: note.discussion_id, - individual_note: true, - notes: [note], - reply_id: note.discussion_id, - }], + notes: [noteData], }); + expect(state.notes.length).toBe(1); + }); + + it('should not add the same note to the notes array', () => { + mutations.ADD_NEW_NOTE(state, note); + expect(state.notes.length).toBe(1); }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 8c5ad8914b0..3e791a31604 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -770,6 +770,20 @@ import '~/notes'; expect($tempNote.prop('nodeName')).toEqual('LI'); expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); }); + + it('should return a escaped user name', () => { + const currentUserFullnameXSS = 'Foo <script>alert("XSS")</script>'; + const $tempNote = this.notes.createPlaceholderNote({ + formContent: sampleComment, + uniqueId, + isDiscussionNote: false, + currentUsername, + currentUserFullname: currentUserFullnameXSS, + currentUserAvatar, + }); + const $tempNoteHeader = $tempNote.find('.note-header'); + expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual('Foo <script>alert("XSS")</script>'); + }); }); describe('createPlaceholderSystemNote', () => { diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index bb47a28d9fe..6611b74594f 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -11,6 +11,7 @@ describe('Pipelines Empty State', () => { component = new EmptyStateComponent({ propsData: { helpPagePath: 'foo', + emptyStateSvgPath: 'foo', }, }).$mount(); }); diff --git a/spec/javascripts/pipelines/error_state_spec.js b/spec/javascripts/pipelines/error_state_spec.js index f667d351f72..a402857a4d1 100644 --- a/spec/javascripts/pipelines/error_state_spec.js +++ b/spec/javascripts/pipelines/error_state_spec.js @@ -8,7 +8,11 @@ describe('Pipelines Error State', () => { beforeEach(() => { ErrorStateComponent = Vue.extend(errorStateComp); - component = new ErrorStateComponent().$mount(); + component = new ErrorStateComponent({ + propsData: { + errorStateSvgPath: 'foo', + }, + }).$mount(); }); it('should render error state SVG', () => { diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 256fdbe743c..4a4f2259d23 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -125,4 +125,23 @@ describe('Pipeline Url Component', () => { component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(), ).toEqual('Auto DevOps'); }); + + it('should render error badge when pipeline has a failure reason set', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: { + failure_reason: true, + }, + failure_reason: 'some reason', + }, + autoDevopsHelpPath: 'foo', + }, + }).$mount(); + + expect(component.$el.querySelector('.js-pipeline-url-failure').textContent).toContain('error'); + expect(component.$el.querySelector('.js-pipeline-url-failure').getAttribute('data-original-title')).toContain('some reason'); + }); }); diff --git a/spec/javascripts/pipelines/pipelines_artifacts_spec.js b/spec/javascripts/pipelines/pipelines_artifacts_spec.js index acb67d0ec21..a8a8e3e2cff 100644 --- a/spec/javascripts/pipelines/pipelines_artifacts_spec.js +++ b/spec/javascripts/pipelines/pipelines_artifacts_spec.js @@ -34,7 +34,7 @@ describe('Pipelines Artifacts dropdown', () => { ).toEqual(artifacts[0].path); expect( - component.$el.querySelector('.dropdown-menu li a span').textContent, + component.$el.querySelector('.dropdown-menu li a').textContent, ).toContain(artifacts[0].name); }); }); diff --git a/spec/javascripts/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js index 0a6c479a95b..084ffe08917 100644 --- a/spec/javascripts/pretty_time_spec.js +++ b/spec/javascripts/pretty_time_spec.js @@ -1,215 +1,133 @@ -import '~/lib/utils/pretty_time'; +import { parseSeconds, abbreviateTime, stringifyTime } from '~/lib/utils/pretty_time'; -(() => { - const prettyTime = gl.utils.prettyTime; +function assertTimeUnits(obj, minutes, hours, days, weeks) { + expect(obj.minutes).toBe(minutes); + expect(obj.hours).toBe(hours); + expect(obj.days).toBe(days); + expect(obj.weeks).toBe(weeks); +} - describe('prettyTime methods', function () { - describe('parseSeconds', function () { - it('should correctly parse a negative value', function () { - const parser = prettyTime.parseSeconds; +describe('prettyTime methods', () => { + describe('parseSeconds', () => { + it('should correctly parse a negative value', () => { + const zeroSeconds = parseSeconds(-1000); - const zeroSeconds = parser(-1000); - - expect(zeroSeconds.minutes).toBe(16); - expect(zeroSeconds.hours).toBe(0); - expect(zeroSeconds.days).toBe(0); - expect(zeroSeconds.weeks).toBe(0); - }); - - it('should correctly parse a zero value', function () { - const parser = prettyTime.parseSeconds; - - const zeroSeconds = parser(0); - - expect(zeroSeconds.minutes).toBe(0); - expect(zeroSeconds.hours).toBe(0); - expect(zeroSeconds.days).toBe(0); - expect(zeroSeconds.weeks).toBe(0); - }); - - it('should correctly parse a small non-zero second values', function () { - const parser = prettyTime.parseSeconds; - - const subOneMinute = parser(10); - - expect(subOneMinute.minutes).toBe(0); - expect(subOneMinute.hours).toBe(0); - expect(subOneMinute.days).toBe(0); - expect(subOneMinute.weeks).toBe(0); - - const aboveOneMinute = parser(100); - - expect(aboveOneMinute.minutes).toBe(1); - expect(aboveOneMinute.hours).toBe(0); - expect(aboveOneMinute.days).toBe(0); - expect(aboveOneMinute.weeks).toBe(0); - - const manyMinutes = parser(1000); - - expect(manyMinutes.minutes).toBe(16); - expect(manyMinutes.hours).toBe(0); - expect(manyMinutes.days).toBe(0); - expect(manyMinutes.weeks).toBe(0); - }); - - it('should correctly parse large second values', function () { - const parser = prettyTime.parseSeconds; - - const aboveOneHour = parser(4800); - - expect(aboveOneHour.minutes).toBe(20); - expect(aboveOneHour.hours).toBe(1); - expect(aboveOneHour.days).toBe(0); - expect(aboveOneHour.weeks).toBe(0); - - const aboveOneDay = parser(110000); - - expect(aboveOneDay.minutes).toBe(33); - expect(aboveOneDay.hours).toBe(6); - expect(aboveOneDay.days).toBe(3); - expect(aboveOneDay.weeks).toBe(0); - - const aboveOneWeek = parser(25000000); - - expect(aboveOneWeek.minutes).toBe(26); - expect(aboveOneWeek.hours).toBe(0); - expect(aboveOneWeek.days).toBe(3); - expect(aboveOneWeek.weeks).toBe(173); - }); + assertTimeUnits(zeroSeconds, 16, 0, 0, 0); + }); - it('should correctly accept a custom param for hoursPerDay', function () { - const parser = prettyTime.parseSeconds; - const config = { hoursPerDay: 24 }; + it('should correctly parse a zero value', () => { + const zeroSeconds = parseSeconds(0); - const aboveOneHour = parser(4800, config); + assertTimeUnits(zeroSeconds, 0, 0, 0, 0); + }); - expect(aboveOneHour.minutes).toBe(20); - expect(aboveOneHour.hours).toBe(1); - expect(aboveOneHour.days).toBe(0); - expect(aboveOneHour.weeks).toBe(0); + it('should correctly parse a small non-zero second values', () => { + const subOneMinute = parseSeconds(10); + const aboveOneMinute = parseSeconds(100); + const manyMinutes = parseSeconds(1000); - const aboveOneDay = parser(110000, config); + assertTimeUnits(subOneMinute, 0, 0, 0, 0); + assertTimeUnits(aboveOneMinute, 1, 0, 0, 0); + assertTimeUnits(manyMinutes, 16, 0, 0, 0); + }); - expect(aboveOneDay.minutes).toBe(33); - expect(aboveOneDay.hours).toBe(6); - expect(aboveOneDay.days).toBe(1); - expect(aboveOneDay.weeks).toBe(0); + it('should correctly parse large second values', () => { + const aboveOneHour = parseSeconds(4800); + const aboveOneDay = parseSeconds(110000); + const aboveOneWeek = parseSeconds(25000000); - const aboveOneWeek = parser(25000000, config); + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 3, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 3, 173); + }); - expect(aboveOneWeek.minutes).toBe(26); - expect(aboveOneWeek.hours).toBe(8); - expect(aboveOneWeek.days).toBe(4); + it('should correctly accept a custom param for hoursPerDay', () => { + const config = { hoursPerDay: 24 }; - expect(aboveOneWeek.weeks).toBe(57); - }); + const aboveOneHour = parseSeconds(4800, config); + const aboveOneDay = parseSeconds(110000, config); + const aboveOneWeek = parseSeconds(25000000, config); - it('should correctly accept a custom param for daysPerWeek', function () { - const parser = prettyTime.parseSeconds; - const config = { daysPerWeek: 7 }; + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 1, 0); + assertTimeUnits(aboveOneWeek, 26, 8, 4, 57); + }); - const aboveOneHour = parser(4800, config); + it('should correctly accept a custom param for daysPerWeek', () => { + const config = { daysPerWeek: 7 }; - expect(aboveOneHour.minutes).toBe(20); - expect(aboveOneHour.hours).toBe(1); - expect(aboveOneHour.days).toBe(0); - expect(aboveOneHour.weeks).toBe(0); + const aboveOneHour = parseSeconds(4800, config); + const aboveOneDay = parseSeconds(110000, config); + const aboveOneWeek = parseSeconds(25000000, config); - const aboveOneDay = parser(110000, config); + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 3, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 0, 124); + }); - expect(aboveOneDay.minutes).toBe(33); - expect(aboveOneDay.hours).toBe(6); - expect(aboveOneDay.days).toBe(3); - expect(aboveOneDay.weeks).toBe(0); + it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => { + const config = { daysPerWeek: 55, hoursPerDay: 14 }; - const aboveOneWeek = parser(25000000, config); + const aboveOneHour = parseSeconds(4800, config); + const aboveOneDay = parseSeconds(110000, config); + const aboveOneWeek = parseSeconds(25000000, config); - expect(aboveOneWeek.minutes).toBe(26); - expect(aboveOneWeek.hours).toBe(0); - expect(aboveOneWeek.days).toBe(0); + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 2, 2, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 1, 9); + }); + }); - expect(aboveOneWeek.weeks).toBe(124); - }); + describe('stringifyTime', () => { + it('should stringify values with all non-zero units', () => { + const timeObject = { + weeks: 1, + days: 4, + hours: 7, + minutes: 20, + }; - it('should correctly accept custom params for daysPerWeek and hoursPerDay', function () { - const parser = prettyTime.parseSeconds; - const config = { daysPerWeek: 55, hoursPerDay: 14 }; + const timeString = stringifyTime(timeObject); - const aboveOneHour = parser(4800, config); + expect(timeString).toBe('1w 4d 7h 20m'); + }); - expect(aboveOneHour.minutes).toBe(20); - expect(aboveOneHour.hours).toBe(1); - expect(aboveOneHour.days).toBe(0); - expect(aboveOneHour.weeks).toBe(0); + it('should stringify values with some non-zero units', () => { + const timeObject = { + weeks: 0, + days: 4, + hours: 0, + minutes: 20, + }; - const aboveOneDay = parser(110000, config); + const timeString = stringifyTime(timeObject); - expect(aboveOneDay.minutes).toBe(33); - expect(aboveOneDay.hours).toBe(2); - expect(aboveOneDay.days).toBe(2); - expect(aboveOneDay.weeks).toBe(0); + expect(timeString).toBe('4d 20m'); + }); - const aboveOneWeek = parser(25000000, config); + it('should stringify values with no non-zero units', () => { + const timeObject = { + weeks: 0, + days: 0, + hours: 0, + minutes: 0, + }; - expect(aboveOneWeek.minutes).toBe(26); - expect(aboveOneWeek.hours).toBe(0); - expect(aboveOneWeek.days).toBe(1); + const timeString = stringifyTime(timeObject); - expect(aboveOneWeek.weeks).toBe(9); - }); + expect(timeString).toBe('0m'); }); + }); - describe('stringifyTime', function () { - it('should stringify values with all non-zero units', function () { - const timeObject = { - weeks: 1, - days: 4, - hours: 7, - minutes: 20, - }; - - const timeString = prettyTime.stringifyTime(timeObject); - - expect(timeString).toBe('1w 4d 7h 20m'); - }); - - it('should stringify values with some non-zero units', function () { - const timeObject = { - weeks: 0, - days: 4, - hours: 0, - minutes: 20, - }; - - const timeString = prettyTime.stringifyTime(timeObject); - - expect(timeString).toBe('4d 20m'); - }); - - it('should stringify values with no non-zero units', function () { - const timeObject = { - weeks: 0, - days: 0, - hours: 0, - minutes: 0, - }; - - const timeString = prettyTime.stringifyTime(timeObject); - - expect(timeString).toBe('0m'); - }); + describe('abbreviateTime', () => { + it('should abbreviate stringified times for weeks', () => { + const fullTimeString = '1w 3d 4h 5m'; + expect(abbreviateTime(fullTimeString)).toBe('1w'); }); - describe('abbreviateTime', function () { - it('should abbreviate stringified times for weeks', function () { - const fullTimeString = '1w 3d 4h 5m'; - expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w'); - }); - - it('should abbreviate stringified times for non-weeks', function () { - const fullTimeString = '0w 3d 4h 5m'; - expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d'); - }); + it('should abbreviate stringified times for non-weeks', () => { + const fullTimeString = '0w 3d 4h 5m'; + expect(abbreviateTime(fullTimeString)).toBe('3d'); }); }); -})(window.gl || (window.gl = {})); +}); diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js new file mode 100644 index 00000000000..2e94948cfb2 --- /dev/null +++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js @@ -0,0 +1,129 @@ +import Vue from 'vue'; + +import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue'; + +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('DeleteAccountModal component', () => { + const actionUrl = `${gl.TEST_HOST}/delete/user`; + const username = 'hasnoname'; + let Component; + let vm; + + beforeEach(() => { + Component = Vue.extend(deleteAccountModal); + }); + + afterEach(() => { + vm.$destroy(); + }); + + const findElements = () => { + const confirmation = vm.confirmWithPassword ? 'password' : 'username'; + return { + form: vm.$refs.form, + input: vm.$el.querySelector(`[name="${confirmation}"]`), + submitButton: vm.$el.querySelector('.btn-danger'), + }; + }; + + describe('with password confirmation', () => { + beforeEach((done) => { + vm = mountComponent(Component, { + actionUrl, + confirmWithPassword: true, + username, + }); + + vm.isOpen = true; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('does not accept empty password', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = ''; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredPassword).toBe(input.value); + expect(submitButton).toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('submits form with password', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = 'anything'; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredPassword).toBe(input.value); + expect(submitButton).not.toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('with username confirmation', () => { + beforeEach((done) => { + vm = mountComponent(Component, { + actionUrl, + confirmWithPassword: false, + username, + }); + + vm.isOpen = true; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('does not accept wrong username', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = 'this is wrong'; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredUsername).toBe(input.value); + expect(submitButton).toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('submits form with correct username', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = username; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredUsername).toBe(input.value); + expect(submitButton).not.toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/service/projects_service_spec.js b/spec/javascripts/projects_dropdown/service/projects_service_spec.js index d5dd8b3449a..cfd1bb7d24f 100644 --- a/spec/javascripts/projects_dropdown/service/projects_service_spec.js +++ b/spec/javascripts/projects_dropdown/service/projects_service_spec.js @@ -34,7 +34,7 @@ describe('ProjectsService', () => { const searchQuery = 'lab'; const queryParams = { - simple: false, + simple: true, per_page: 20, membership: true, order_by: 'last_activity_at', diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js new file mode 100644 index 00000000000..43e7d9e1224 --- /dev/null +++ b/spec/javascripts/registry/components/app_spec.js @@ -0,0 +1,122 @@ +import Vue from 'vue'; +import registry from '~/registry/components/app.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { reposServerResponse } from '../mock_data'; + +describe('Registry List', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(registry); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render a list of repos', (done) => { + setTimeout(() => { + expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length); + + Vue.nextTick(() => { + expect( + vm.$el.querySelectorAll('.container-image').length, + ).toEqual(reposServerResponse.length); + done(); + }); + }, 0); + }); + + describe('delete repository', () => { + it('should be possible to delete a repo', (done) => { + setTimeout(() => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined(); + done(); + }); + }, 0); + }); + }); + + describe('toggle repository', () => { + it('should open the container', (done) => { + setTimeout(() => { + Vue.nextTick(() => { + vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual('fa fa-chevron-up'); + done(); + }); + }); + }, 0); + }); + }); + }); + + describe('without data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render empty message', (done) => { + setTimeout(() => { + expect( + vm.$el.querySelector('p').textContent.trim(), + ).toEqual('No container images stored for this project. Add one by following the instructions above.'); + done(); + }, 0); + }); + }); + + describe('while loading data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render a loading spinner', (done) => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.fa-spinner')).not.toBe(null); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js new file mode 100644 index 00000000000..5891921318a --- /dev/null +++ b/spec/javascripts/registry/components/collapsible_container_spec.js @@ -0,0 +1,58 @@ +import Vue from 'vue'; +import collapsibleComponent from '~/registry/components/collapsible_container.vue'; +import store from '~/registry/stores'; +import { repoPropsData } from '../mock_data'; + +describe('collapsible registry container', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(collapsibleComponent); + vm = new Component({ + store, + propsData: { + repo: repoPropsData, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('toggle', () => { + it('should be closed by default', () => { + expect(vm.$el.querySelector('.container-image-tags')).toBe(null); + expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right'); + }); + + it('should be open when user clicks on closed repo', (done) => { + vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-tags')).toBeDefined(); + expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-up'); + done(); + }); + }); + + it('should be closed when the user clicks on an opened repo', (done) => { + vm.$el.querySelector('.js-toggle-repo').click(); + + Vue.nextTick(() => { + vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-tags')).toBe(null); + expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right'); + done(); + }); + }); + }); + }); + + describe('delete repo', () => { + it('should be possible to delete a repo', () => { + expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/registry/components/table_registry_spec.js b/spec/javascripts/registry/components/table_registry_spec.js new file mode 100644 index 00000000000..6aa61afc445 --- /dev/null +++ b/spec/javascripts/registry/components/table_registry_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import tableRegistry from '~/registry/components/table_registry.vue'; +import store from '~/registry/stores'; +import { repoPropsData } from '../mock_data'; + +describe('table registry', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(tableRegistry); + vm = new Component({ + store, + propsData: { + repo: repoPropsData, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + 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(/\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 be possible to delete a registry', () => { + expect( + vm.$el.querySelector('.table tbody tr .js-delete-registry'), + ).toBeDefined(); + }); + + describe('pagination', () => { + it('should be possible to change the page', () => { + expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/registry/getters_spec.js b/spec/javascripts/registry/getters_spec.js new file mode 100644 index 00000000000..3d989541881 --- /dev/null +++ b/spec/javascripts/registry/getters_spec.js @@ -0,0 +1,43 @@ +import * as getters from '~/registry/stores/getters'; + +describe('Getters Registry Store', () => { + let state; + + beforeEach(() => { + state = { + isLoading: false, + endpoint: '/root/empty-project/container_registry.json', + repos: [{ + canDelete: true, + destroyPath: 'bar', + id: '134', + isLoading: false, + list: [], + location: 'foo', + name: 'gitlab-org/omnibus-gitlab/foo', + tagsPath: 'foo', + }, { + canDelete: true, + destroyPath: 'bar', + id: '123', + isLoading: false, + list: [], + location: 'foo', + name: 'gitlab-org/omnibus-gitlab', + tagsPath: 'foo', + }], + }; + }); + + describe('isLoading', () => { + it('should return the isLoading property', () => { + expect(getters.isLoading(state)).toEqual(state.isLoading); + }); + }); + + describe('repos', () => { + it('should return the repos', () => { + expect(getters.repos(state)).toEqual(state.repos); + }); + }); +}); diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js new file mode 100644 index 00000000000..18600d00bff --- /dev/null +++ b/spec/javascripts/registry/mock_data.js @@ -0,0 +1,122 @@ +export const defaultState = { + isLoading: false, + endpoint: '', + repos: [], +}; + +export const reposServerResponse = [ + { + destroy_path: 'path', + id: '123', + location: 'location', + path: 'foo', + tags_path: 'tags_path', + }, + { + destroy_path: 'path_', + id: '456', + location: 'location_', + path: 'bar', + tags_path: 'tags_path_', + }, +]; + +export const registryServerResponse = [ + { + name: 'centos7', + short_revision: 'b118ab5b0', + revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', + size: 679, + layers: 19, + location: 'location', + created_at: 1505828744434, + destroy_path: 'path_', + }, + { + name: 'centos6', + short_revision: 'b118ab5b0', + revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', + size: 679, + layers: 19, + location: 'location', + created_at: 1505828744434, + }]; + +export const parsedReposServerResponse = [ + { + canDelete: true, + destroyPath: reposServerResponse[0].destroy_path, + id: reposServerResponse[0].id, + isLoading: false, + list: [], + location: reposServerResponse[0].location, + name: reposServerResponse[0].path, + tagsPath: reposServerResponse[0].tags_path, + }, + { + canDelete: true, + destroyPath: reposServerResponse[1].destroy_path, + id: reposServerResponse[1].id, + isLoading: false, + list: [], + location: reposServerResponse[1].location, + name: reposServerResponse[1].path, + tagsPath: reposServerResponse[1].tags_path, + }, +]; + +export const parsedRegistryServerResponse = [ + { + tag: registryServerResponse[0].name, + revision: registryServerResponse[0].revision, + shortRevision: registryServerResponse[0].short_revision, + size: registryServerResponse[0].size, + layers: registryServerResponse[0].layers, + location: registryServerResponse[0].location, + createdAt: registryServerResponse[0].created_at, + destroyPath: registryServerResponse[0].destroy_path, + canDelete: true, + }, + { + tag: registryServerResponse[1].name, + revision: registryServerResponse[1].revision, + shortRevision: registryServerResponse[1].short_revision, + size: registryServerResponse[1].size, + layers: registryServerResponse[1].layers, + location: registryServerResponse[1].location, + createdAt: registryServerResponse[1].created_at, + destroyPath: registryServerResponse[1].destroy_path, + canDelete: false, + }, +]; + +export const repoPropsData = { + canDelete: true, + destroyPath: 'path', + id: '123', + isLoading: false, + list: [ + { + tag: 'centos6', + revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', + shortRevision: 'b118ab5b0', + size: 19, + layers: 10, + location: 'location', + createdAt: 1505828744434, + destroyPath: 'path', + canDelete: true, + }, + ], + location: 'location', + name: 'foo', + tagsPath: 'path', + pagination: { + perPage: 5, + page: 1, + total: 13, + totalPages: 1, + nextPage: null, + previousPage: null, + }, +}; diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js new file mode 100644 index 00000000000..3c9da4f107b --- /dev/null +++ b/spec/javascripts/registry/stores/actions_spec.js @@ -0,0 +1,85 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import _ from 'underscore'; +import * as actions from '~/registry/stores/actions'; +import * as types from '~/registry/stores/mutation_types'; +import testAction from '../../helpers/vuex_action_helper'; +import { + defaultState, + reposServerResponse, + registryServerResponse, + parsedReposServerResponse, +} from '../mock_data'; + +Vue.use(VueResource); + +describe('Actions Registry Store', () => { + let interceptor; + let mockedState; + + beforeEach(() => { + mockedState = defaultState; + }); + + describe('server requests', () => { + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + describe('fetchRepos', () => { + beforeEach(() => { + interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + Vue.http.interceptors.push(interceptor); + }); + + it('should set receveived repos', (done) => { + testAction(actions.fetchRepos, null, mockedState, [ + { type: types.TOGGLE_MAIN_LOADING }, + { type: types.SET_REPOS_LIST, payload: reposServerResponse }, + ], done); + }); + }); + + describe('fetchList', () => { + beforeEach(() => { + interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(registryServerResponse), { + status: 200, + })); + }; + + Vue.http.interceptors.push(interceptor); + }); + + it('should set received list', (done) => { + mockedState.repos = parsedReposServerResponse; + + testAction(actions.fetchList, { repo: mockedState.repos[1] }, mockedState, [ + { type: types.TOGGLE_REGISTRY_LIST_LOADING }, + { type: types.SET_REGISTRY_LIST, payload: registryServerResponse }, + ], done); + }); + }); + }); + + describe('setMainEndpoint', () => { + it('should commit set main endpoint', (done) => { + testAction(actions.setMainEndpoint, 'endpoint', mockedState, [ + { type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }, + ], done); + }); + }); + + describe('toggleLoading', () => { + it('should commit toggle main loading', (done) => { + testAction(actions.toggleLoading, null, mockedState, [ + { type: types.TOGGLE_MAIN_LOADING }, + ], done); + }); + }); +}); diff --git a/spec/javascripts/registry/stores/mutations_spec.js b/spec/javascripts/registry/stores/mutations_spec.js new file mode 100644 index 00000000000..2e4c0659daa --- /dev/null +++ b/spec/javascripts/registry/stores/mutations_spec.js @@ -0,0 +1,81 @@ +import mutations from '~/registry/stores/mutations'; +import * as types from '~/registry/stores/mutation_types'; +import { + defaultState, + reposServerResponse, + registryServerResponse, + parsedReposServerResponse, + parsedRegistryServerResponse, +} from '../mock_data'; + +describe('Mutations Registry Store', () => { + let mockState; + beforeEach(() => { + mockState = defaultState; + }); + + describe('SET_MAIN_ENDPOINT', () => { + it('should set the main endpoint', () => { + const expectedState = Object.assign({}, mockState, { endpoint: 'foo' }); + mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo'); + expect(mockState).toEqual(expectedState); + }); + }); + + describe('SET_REPOS_LIST', () => { + it('should set a parsed repository list', () => { + mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); + expect(mockState.repos).toEqual(parsedReposServerResponse); + }); + }); + + describe('TOGGLE_MAIN_LOADING', () => { + it('should set a parsed repository list', () => { + mutations[types.TOGGLE_MAIN_LOADING](mockState); + expect(mockState.isLoading).toEqual(true); + }); + }); + + describe('SET_REGISTRY_LIST', () => { + it('should set a list of registries in a specific repository', () => { + mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); + mutations[types.SET_REGISTRY_LIST](mockState, { + repo: mockState.repos[0], + resp: registryServerResponse, + headers: { + 'x-per-page': 2, + 'x-page': 1, + 'x-total': 10, + }, + }); + + expect(mockState.repos[0].list).toEqual(parsedRegistryServerResponse); + expect(mockState.repos[0].pagination).toEqual({ + perPage: 2, + page: 1, + total: 10, + totalPages: NaN, + nextPage: NaN, + previousPage: NaN, + }); + }); + }); + + describe('TOGGLE_REGISTRY_LIST_LOADING', () => { + it('should toggle isLoading property for a specific repository', () => { + mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); + mutations[types.SET_REGISTRY_LIST](mockState, { + repo: mockState.repos[0], + resp: registryServerResponse, + headers: { + 'x-per-page': 2, + 'x-page': 1, + 'x-total': 10, + }, + }); + + mutations[types.TOGGLE_REGISTRY_LIST_LOADING](mockState, mockState.repos[0]); + expect(mockState.repos[0].isLoading).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js index 29dc2d21e4b..411514009dc 100644 --- a/spec/javascripts/repo/components/repo_edit_button_spec.js +++ b/spec/javascripts/repo/components/repo_edit_button_spec.js @@ -21,13 +21,11 @@ describe('RepoEditButton', () => { expect(vm.$el.textContent).toMatch('Edit'); spyOn(vm, 'editCancelClicked').and.callThrough(); - spyOn(vm, 'toggleProjectRefsForm'); vm.$el.click(); Vue.nextTick(() => { expect(vm.editCancelClicked).toHaveBeenCalled(); - expect(vm.toggleProjectRefsForm).toHaveBeenCalled(); expect(vm.$el.textContent).toMatch('Cancel edit'); done(); }); diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index 518a2d25ecf..620b604f404 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import repoFile from '~/repo/components/repo_file.vue'; +import RepoStore from '~/repo/stores/repo_store'; describe('RepoFile', () => { const updated = 'updated'; @@ -12,8 +13,13 @@ describe('RepoFile', () => { level: 10, }; const activeFile = { + pageTitle: 'pageTitle', url: 'url', }; + const otherFile = { + html: '<p class="file-content">html</p>', + pageTitle: 'otherpageTitle', + }; function createComponent(propsData) { const RepoFile = Vue.extend(repoFile); @@ -23,15 +29,17 @@ describe('RepoFile', () => { }).$mount(); } - beforeEach(() => { - spyOn(repoFile.mixins[0].methods, 'timeFormated').and.returnValue(updated); - }); - it('renders link, icon, name and last commit details', () => { - const vm = createComponent({ - file, - activeFile, + const RepoFile = Vue.extend(repoFile); + const vm = new RepoFile({ + propsData: { + file, + activeFile, + }, }); + spyOn(vm, 'timeFormated').and.returnValue(updated); + vm.$mount(); + const name = vm.$el.querySelector('.repo-file-name'); const fileIcon = vm.$el.querySelector('.file-icon'); @@ -60,6 +68,12 @@ describe('RepoFile', () => { expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); }); + it('sets the document title correctly', () => { + RepoStore.setActiveFiles(otherFile); + + expect(document.title.trim()).toEqual(otherFile.pageTitle); + }); + it('renders a spinner if the file is loading', () => { file.loading = true; const vm = createComponent({ diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index abcff8e537e..35d2b37ac2a 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -5,26 +5,34 @@ import RepoStore from '~/repo/stores/repo_store'; import repoSidebar from '~/repo/components/repo_sidebar.vue'; describe('RepoSidebar', () => { + let vm; + function createComponent() { const RepoSidebar = Vue.extend(repoSidebar); return new RepoSidebar().$mount(); } + afterEach(() => { + vm.$destroy(); + }); + it('renders a sidebar', () => { RepoStore.files = [{ id: 0, }]; RepoStore.openedFiles = []; - const vm = createComponent(); + RepoStore.isRoot = false; + + vm = createComponent(); const thead = vm.$el.querySelector('thead'); const tbody = vm.$el.querySelector('tbody'); expect(vm.$el.id).toEqual('sidebar'); expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy(); expect(thead.querySelector('.name').textContent).toEqual('Name'); - expect(thead.querySelector('.last-commit').textContent).toEqual('Last Commit'); - expect(thead.querySelector('.last-update').textContent).toEqual('Last Update'); + expect(thead.querySelector('.last-commit').textContent).toEqual('Last commit'); + expect(thead.querySelector('.last-update').textContent).toEqual('Last update'); expect(tbody.querySelector('.repo-file-options')).toBeFalsy(); expect(tbody.querySelector('.prev-directory')).toBeFalsy(); expect(tbody.querySelector('.loading-file')).toBeFalsy(); @@ -35,7 +43,7 @@ describe('RepoSidebar', () => { RepoStore.openedFiles = [{ id: 0, }]; - const vm = createComponent(); + vm = createComponent(); expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy(); expect(vm.$el.querySelector('thead')).toBeFalsy(); @@ -47,7 +55,7 @@ describe('RepoSidebar', () => { tree: true, }; RepoStore.files = []; - const vm = createComponent(); + vm = createComponent(); expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5); }); @@ -57,7 +65,7 @@ describe('RepoSidebar', () => { id: 0, }]; RepoStore.isRoot = true; - const vm = createComponent(); + vm = createComponent(); expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); }); @@ -72,13 +80,27 @@ describe('RepoSidebar', () => { }; RepoStore.files = [file1]; RepoStore.isRoot = true; - const vm = createComponent(); + vm = createComponent(); vm.fileClicked(file1); expect(Helper.getContent).toHaveBeenCalledWith(file1); }); + it('should not fetch data for already opened files', () => { + const file = { + id: 42, + url: 'foo', + }; + + spyOn(Helper, 'getFileFromPath').and.returnValue(file); + spyOn(RepoStore, 'setActiveFiles'); + vm = createComponent(); + vm.fileClicked(file); + + expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file); + }); + it('should hide files in directory if already open', () => { spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough(); const file1 = { @@ -89,7 +111,7 @@ describe('RepoSidebar', () => { }; RepoStore.files = [file1]; RepoStore.isRoot = true; - const vm = createComponent(); + vm = createComponent(); vm.fileClicked(file1); @@ -100,12 +122,48 @@ describe('RepoSidebar', () => { describe('goToPreviousDirectoryClicked', () => { it('should hide files in directory if already open', () => { const prevUrl = 'foo/bar'; - const vm = createComponent(); + vm = createComponent(); vm.goToPreviousDirectoryClicked(prevUrl); expect(RepoService.url).toEqual(prevUrl); }); }); + + describe('back button', () => { + const file1 = { + id: 1, + url: 'file1', + }; + const file2 = { + id: 2, + url: 'file2', + }; + RepoStore.files = [file1, file2]; + RepoStore.openedFiles = [file1, file2]; + RepoStore.isRoot = true; + + vm = createComponent(); + vm.fileClicked(file1); + + it('render previous file when using back button', () => { + spyOn(Helper, 'getContent').and.callThrough(); + + vm.fileClicked(file2); + expect(Helper.getContent).toHaveBeenCalledWith(file2); + Helper.getContent.calls.reset(); + + history.pushState({ + key: Math.random(), + }, '', file1.url); + const popEvent = document.createEvent('Event'); + popEvent.initEvent('popstate', true, true); + window.dispatchEvent(popEvent); + + expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(file1.url); + + window.history.pushState({}, null, '/'); + }); + }); }); }); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index f2072a6f350..5505f983d71 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -32,56 +32,86 @@ import '~/right_sidebar'; }; describe('RightSidebar', function() { - var fixtureName = 'issues/open-issue.html.raw'; - preloadFixtures(fixtureName); - loadJSONFixtures('todos/todos.json'); - - beforeEach(function() { - loadFixtures(fixtureName); - this.sidebar = new Sidebar; - $aside = $('.right-sidebar'); - $page = $('.page-with-sidebar'); - $icon = $aside.find('i'); - $toggle = $aside.find('.js-sidebar-toggle'); - return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); - }); - it('should expand/collapse the sidebar when arrow is clicked', function() { - assertSidebarState('expanded'); - $toggle.click(); - assertSidebarState('collapsed'); - $toggle.click(); - assertSidebarState('expanded'); - }); - it('should float over the page and when sidebar icons clicked', function() { - $labelsIcon.click(); - return assertSidebarState('expanded'); - }); - it('should collapse when the icon arrow clicked while it is floating on page', function() { - $labelsIcon.click(); - assertSidebarState('expanded'); - $toggle.click(); - return assertSidebarState('collapsed'); + describe('fixture tests', () => { + var fixtureName = 'issues/open-issue.html.raw'; + preloadFixtures(fixtureName); + loadJSONFixtures('todos/todos.json'); + + beforeEach(function() { + loadFixtures(fixtureName); + this.sidebar = new Sidebar; + $aside = $('.right-sidebar'); + $page = $('.page-with-sidebar'); + $icon = $aside.find('i'); + $toggle = $aside.find('.js-sidebar-toggle'); + return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); + }); + it('should expand/collapse the sidebar when arrow is clicked', function() { + assertSidebarState('expanded'); + $toggle.click(); + assertSidebarState('collapsed'); + $toggle.click(); + assertSidebarState('expanded'); + }); + it('should float over the page and when sidebar icons clicked', function() { + $labelsIcon.click(); + return assertSidebarState('expanded'); + }); + it('should collapse when the icon arrow clicked while it is floating on page', function() { + $labelsIcon.click(); + assertSidebarState('expanded'); + $toggle.click(); + return assertSidebarState('collapsed'); + }); + + it('should broadcast todo:toggle event when add todo clicked', function() { + var todos = getJSONFixture('todos/todos.json'); + spyOn(jQuery, 'ajax').and.callFake(function() { + var d = $.Deferred(); + var response = todos; + d.resolve(response); + return d.promise(); + }); + + var todoToggleSpy = spyOnEvent(document, 'todo:toggle'); + + $('.issuable-sidebar-header .js-issuable-todo').click(); + + expect(todoToggleSpy.calls.count()).toEqual(1); + }); + + it('should not hide collapsed icons', () => { + [].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => { + expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy(); + }); + }); }); - it('should broadcast todo:toggle event when add todo clicked', function() { - var todos = getJSONFixture('todos/todos.json'); - spyOn(jQuery, 'ajax').and.callFake(function() { - var d = $.Deferred(); - var response = todos; - d.resolve(response); - return d.promise(); + describe('sidebarToggleClicked', () => { + const event = jasmine.createSpyObj('event', ['preventDefault']); + + beforeEach(() => { + spyOn($.fn, 'hasClass').and.returnValue(false); + }); + + afterEach(() => { + gl.lazyLoader = undefined; }); - var todoToggleSpy = spyOnEvent(document, 'todo:toggle'); + it('calls loadCheck if lazyLoader is set', () => { + gl.lazyLoader = jasmine.createSpyObj('lazyLoader', ['loadCheck']); - $('.issuable-sidebar-header .js-issuable-todo').click(); + Sidebar.prototype.sidebarToggleClicked(event); - expect(todoToggleSpy.calls.count()).toEqual(1); - }); + expect(gl.lazyLoader.loadCheck).toHaveBeenCalled(); + }); + + it('does not throw if lazyLoader is not defined', () => { + gl.lazyLoader = undefined; + + const toggle = Sidebar.prototype.sidebarToggleClicked.bind(null, event); - it('should not hide collapsed icons', () => { - [].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => { - expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy(); + expect(toggle).not.toThrow(); }); }); }); diff --git a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js new file mode 100644 index 00000000000..b0ea8ae0206 --- /dev/null +++ b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('EditFormButtons', () => { + let vm1; + let vm2; + + beforeEach(() => { + const Component = Vue.extend(editFormButtons); + const toggleForm = () => { }; + const updateLockedAttribute = () => { }; + + vm1 = mountComponent(Component, { + isLocked: true, + toggleForm, + updateLockedAttribute, + }); + + vm2 = mountComponent(Component, { + isLocked: false, + toggleForm, + updateLockedAttribute, + }); + }); + + it('renders unlock or lock text based on locked state', () => { + expect( + vm1.$el.innerHTML.includes('Unlock'), + ).toBe(true); + + expect( + vm2.$el.innerHTML.includes('Lock'), + ).toBe(true); + }); +}); diff --git a/spec/javascripts/sidebar/lock/edit_form_spec.js b/spec/javascripts/sidebar/lock/edit_form_spec.js new file mode 100644 index 00000000000..7abd6997a18 --- /dev/null +++ b/spec/javascripts/sidebar/lock/edit_form_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import editForm from '~/sidebar/components/lock/edit_form.vue'; + +describe('EditForm', () => { + let vm1; + let vm2; + + beforeEach(() => { + const Component = Vue.extend(editForm); + const toggleForm = () => { }; + const updateLockedAttribute = () => { }; + + vm1 = new Component({ + propsData: { + isLocked: true, + toggleForm, + updateLockedAttribute, + issuableType: 'issue', + }, + }).$mount(); + + vm2 = new Component({ + propsData: { + isLocked: false, + toggleForm, + updateLockedAttribute, + issuableType: 'merge_request', + }, + }).$mount(); + }); + + it('renders on the appropriate warning text', () => { + expect( + vm1.$el.innerHTML.includes('Unlock this issue?'), + ).toBe(true); + + expect( + vm2.$el.innerHTML.includes('Lock this merge request?'), + ).toBe(true); + }); +}); diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js new file mode 100644 index 00000000000..696fca516bc --- /dev/null +++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js @@ -0,0 +1,71 @@ +import Vue from 'vue'; +import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue'; + +describe('LockIssueSidebar', () => { + let vm1; + let vm2; + + beforeEach(() => { + const Component = Vue.extend(lockIssueSidebar); + + const mediator = { + service: { + update: Promise.resolve(true), + }, + + store: { + isLockDialogOpen: false, + }, + }; + + vm1 = new Component({ + propsData: { + isLocked: true, + isEditable: true, + mediator, + issuableType: 'issue', + }, + }).$mount(); + + vm2 = new Component({ + propsData: { + isLocked: false, + isEditable: false, + mediator, + issuableType: 'merge_request', + }, + }).$mount(); + }); + + it('shows if locked and/or editable', () => { + expect( + vm1.$el.innerHTML.includes('Edit'), + ).toBe(true); + + expect( + vm1.$el.innerHTML.includes('Locked'), + ).toBe(true); + + expect( + vm2.$el.innerHTML.includes('Unlocked'), + ).toBe(true); + }); + + it('displays the edit form when editable', (done) => { + expect(vm1.isLockDialogOpen).toBe(false); + + vm1.$el.querySelector('.lock-edit').click(); + + expect(vm1.isLockDialogOpen).toBe(true); + + vm1.$nextTick(() => { + expect( + vm1.$el + .innerHTML + .includes('Unlock this issue?'), + ).toBe(true); + + done(); + }); + }); +}); diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index fd492159081..7d3c9319a11 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -26,37 +26,30 @@ describe('Todos', () => { describe('meta click', () => { let visitUrlSpy; + let windowOpenSpy; + let metakeyEvent; beforeEach(() => { - spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {}); + windowOpenSpy = spyOn(window, 'open').and.callFake(() => {}); }); - it('opens the todo url in another tab', (done) => { + it('opens the todo url in another tab', () => { const todoLink = todoItem.dataset.url; - spyOn(window, 'open').and.callFake((url, target) => { - expect(todoLink).toEqual(url); - expect(target).toEqual('_blank'); - done(); - }); + $('.todos-list .todo').trigger(metakeyEvent); - todoItem.click(); expect(visitUrlSpy).not.toHaveBeenCalled(); + expect(windowOpenSpy).toHaveBeenCalledWith(todoLink, '_blank'); }); - it('opens the avatar\'s url in another tab when the avatar is clicked', (done) => { - const avatarImage = todoItem.querySelector('img'); - const avatarUrl = avatarImage.parentElement.getAttribute('href'); + it('run native funcionality when avatar is clicked', () => { + $('.todos-list a').on('click', e => e.preventDefault()); + $('.todos-list img').trigger(metakeyEvent); - spyOn(window, 'open').and.callFake((url, target) => { - expect(avatarUrl).toEqual(url); - expect(target).toEqual('_blank'); - done(); - }); - - avatarImage.click(); expect(visitUrlSpy).not.toHaveBeenCalled(); + expect(windowOpenSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js deleted file mode 100644 index 69cb93bd850..00000000000 --- a/spec/javascripts/user_callout_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import Cookies from 'js-cookie'; -import UserCallout from '~/user_callout'; - -const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; - -describe('UserCallout', function () { - const fixtureName = 'dashboard/user-callout.html.raw'; - preloadFixtures(fixtureName); - - beforeEach(() => { - loadFixtures(fixtureName); - Cookies.remove(USER_CALLOUT_COOKIE); - - this.userCallout = new UserCallout(); - this.closeButton = $('.js-close-callout.close'); - this.userCalloutBtn = $('.js-close-callout:not(.close)'); - }); - - it('hides when user clicks on the dismiss-icon', (done) => { - this.closeButton.click(); - expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); - - setTimeout(() => { - expect( - document.querySelector('.user-callout'), - ).toBeNull(); - - done(); - }); - }); - - it('hides when user clicks on the "check it out" button', () => { - this.userCalloutBtn.click(); - expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); - }); - - describe('Sets cookie with setCalloutPerProject', () => { - beforeEach(() => { - spyOn(Cookies, 'set').and.callFake(() => {}); - document.querySelector('.user-callout').setAttribute('data-project-path', 'foo/bar'); - this.userCallout = new UserCallout({ setCalloutPerProject: true }); - }); - - it('sets a cookie when the user clicks the close button', () => { - this.userCalloutBtn.click(); - expect(Cookies.set).toHaveBeenCalledWith('user_callout_dismissed', 'true', Object({ expires: 365, path: 'foo/bar' })); - }); - }); -}); 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 c763487d12f..690665ae12c 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 @@ -37,6 +37,26 @@ describe('MRWidgetPipeline', () => { }); }); + describe('hasPipeline', () => { + it('should return true when there is a pipeline', () => { + expect(Object.keys(mockData.pipeline).length).toBeGreaterThan(0); + + const vm = createComponent({ + pipeline: mockData.pipeline, + }); + + expect(vm.hasPipeline).toBeTruthy(); + }); + + it('should return false when there is no pipeline', () => { + const vm = createComponent({ + pipeline: null, + }); + + expect(vm.hasPipeline).toBeFalsy(); + }); + }); + describe('hasCIError', () => { it('should return false when there is no CI error', () => { const vm = createComponent({ diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js index 47303d1e80f..d23b558f4ea 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -4,11 +4,15 @@ import closedComponent from '~/vue_merge_request_widget/components/states/mr_wid const mr = { targetBranch: 'good-branch', targetBranchPath: '/good-branch', - closedBy: { - name: 'Fatih Acet', - username: 'fatihacet', + closedEvent: { + author: { + name: 'Fatih Acet', + username: 'fatihacet', + }, + updatedAt: 'closedEventUpdatedAt', + formattedUpdatedAt: '', }, - updatedAt: '2017-03-23T20:08:08.845Z', + updatedAt: 'mrUpdatedAt', closedAt: '1 day ago', }; @@ -18,7 +22,7 @@ const createComponent = () => { return new Component({ el: document.createElement('div'), propsData: { mr }, - }).$el; + }); }; describe('MRWidgetClosed', () => { @@ -38,14 +42,30 @@ describe('MRWidgetClosed', () => { }); describe('template', () => { - it('should have correct elements', () => { - const el = createComponent(); + let vm; + let el; + beforeEach(() => { + vm = createComponent(); + el = vm.$el; + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should have correct elements', () => { expect(el.querySelector('h4').textContent).toContain('Closed by'); - expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name); + expect(el.querySelector('h4').textContent).toContain(mr.closedEvent.author.name); expect(el.textContent).toContain('The changes were not merged into'); expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath); expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch); }); + + it('should use closedEvent updatedAt as tooltip title', () => { + expect( + el.querySelector('time').getAttribute('title'), + ).toBe('closedEventUpdatedAt'); + }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index 3b7b7d93662..5d4c7ec09dc 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -1,20 +1,9 @@ import Vue from 'vue'; import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; +const ConflictsComponent = Vue.extend(conflictsComponent); const path = '/conflicts'; -const createComponent = () => { - const Component = Vue.extend(conflictsComponent); - - return new Component({ - el: document.createElement('div'), - propsData: { - mr: { - canMerge: true, - conflictResolutionPath: path, - }, - }, - }); -}; describe('MRWidgetConflicts', () => { describe('props', () => { @@ -27,44 +16,90 @@ describe('MRWidgetConflicts', () => { }); describe('template', () => { - it('should have correct elements', () => { - const el = createComponent().$el; - const resolveButton = el.querySelector('.js-resolve-conflicts-button'); - const mergeButton = el.querySelector('.mr-widget-body .btn'); - const mergeLocallyButton = el.querySelector('.js-merge-locally-button'); - - expect(el.textContent).toContain('There are merge conflicts'); - expect(el.textContent).not.toContain('ask someone with write access'); - expect(el.querySelector('.btn-success').disabled).toBeTruthy(); - expect(resolveButton.textContent).toContain('Resolve conflicts'); - expect(resolveButton.getAttribute('href')).toEqual(path); - expect(mergeButton.textContent).toContain('Merge'); - expect(mergeLocallyButton.textContent).toContain('Merge locally'); + describe('when allowed to merge', () => { + let vm; + + beforeEach(() => { + vm = mountComponent(ConflictsComponent, { + mr: { + canMerge: true, + conflictResolutionPath: path, + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should tell you about conflicts without bothering other people', () => { + expect(vm.$el.textContent).toContain('There are merge conflicts'); + expect(vm.$el.textContent).not.toContain('ask someone with write access'); + }); + + it('should allow you to resolve the conflicts', () => { + const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); + + expect(resolveButton.textContent).toContain('Resolve conflicts'); + expect(resolveButton.getAttribute('href')).toEqual(path); + }); + + it('should have merge buttons', () => { + const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); + const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button'); + + expect(mergeButton.textContent).toContain('Merge'); + expect(mergeButton.disabled).toBeTruthy(); + expect(mergeButton.classList.contains('btn-success')).toEqual(true); + expect(mergeLocallyButton.textContent).toContain('Merge locally'); + }); }); describe('when user does not have permission to merge', () => { let vm; beforeEach(() => { - vm = createComponent(); - vm.mr.canMerge = false; + vm = mountComponent(ConflictsComponent, { + mr: { + canMerge: false, + }, + }); }); - it('should show proper message', (done) => { - Vue.nextTick(() => { - expect(vm.$el.textContent).toContain('ask someone with write access'); - done(); - }); + afterEach(() => { + vm.$destroy(); + }); + + it('should show proper message', () => { + expect(vm.$el.textContent).toContain('ask someone with write access'); + }); + + it('should not have action buttons', () => { + expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); + expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); + expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); }); + }); - it('should not have action buttons', (done) => { - Vue.nextTick(() => { - expect(vm.$el.querySelectorAll('.btn').length).toBe(1); - expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toEqual(null); - expect(vm.$el.querySelector('.js-merge-locally-button')).toEqual(null); - done(); + describe('when fast-forward or semi-linear merge enabled', () => { + let vm; + + beforeEach(() => { + vm = mountComponent(ConflictsComponent, { + mr: { + shouldBeRebased: true, + }, }); }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should tell you to rebase locally', () => { + expect(vm.$el.textContent).toContain('Fast-forward merge is not possible.'); + expect(vm.$el.textContent).toContain('To merge this request, first rebase locally'); + }); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index afaa750199a..2714e8294fa 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -14,9 +14,12 @@ const createComponent = () => { canRevertInCurrentMR: true, canRemoveSourceBranch: true, sourceBranchRemoved: true, - mergedBy: {}, - mergedAt: '', - updatedAt: '', + mergedEvent: { + author: {}, + updatedAt: 'mergedUpdatedAt', + formattedUpdatedAt: '', + }, + updatedAt: 'mrUpdatedAt', targetBranch, }; @@ -170,5 +173,11 @@ describe('MRWidgetMerged', () => { done(); }); }); + + it('should use mergedEvent updatedAt as tooltip title', () => { + expect( + el.querySelector('time').getAttribute('title'), + ).toBe('mergedUpdatedAt'); + }); }); }); 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 c607c9746a4..d7019ea408b 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 @@ -11,6 +11,8 @@ const createComponent = (customConfig = {}) => { isPipelineActive: false, pipeline: null, isPipelineFailed: false, + isPipelinePassing: false, + isMergeAllowed: true, onlyAllowMergeIfPipelineSucceeds: false, hasCI: false, ciStatus: null, @@ -68,6 +70,18 @@ describe('MRWidgetReadyToMerge', () => { }); describe('computed', () => { + describe('shouldShowMergeWhenPipelineSucceedsText', () => { + it('should return true with active pipeline', () => { + vm.mr.isPipelineActive = true; + expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeTruthy(); + }); + + it('should return false with inactive pipeline', () => { + vm.mr.isPipelineActive = false; + expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeFalsy(); + }); + }); + describe('commitMessageLinkTitle', () => { const withDesc = 'Include description in commit message'; const withoutDesc = "Don't include description in commit message"; @@ -82,35 +96,84 @@ describe('MRWidgetReadyToMerge', () => { }); }); + describe('status', () => { + it('defaults to success', () => { + vm.mr.pipeline = true; + expect(vm.status).toEqual('success'); + }); + + it('returns failed when MR has CI but also has an unknown status', () => { + vm.mr.hasCI = true; + expect(vm.status).toEqual('failed'); + }); + + it('returns default when MR has no pipeline', () => { + expect(vm.status).toEqual('success'); + }); + + it('returns pending when pipeline is active', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineActive = true; + expect(vm.status).toEqual('pending'); + }); + + it('returns failed when pipeline is failed', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineFailed = true; + expect(vm.status).toEqual('failed'); + }); + }); + describe('mergeButtonClass', () => { - const defaultClass = 'btn btn-small btn-success accept-merge-request'; + const defaultClass = 'btn btn-sm btn-success accept-merge-request'; const failedClass = `${defaultClass} btn-danger`; const inActionClass = `${defaultClass} btn-info`; - it('should return default class', () => { + it('defaults to success class', () => { + expect(vm.mergeButtonClass).toEqual(defaultClass); + }); + + it('returns success class for success status', () => { vm.mr.pipeline = true; expect(vm.mergeButtonClass).toEqual(defaultClass); }); - it('should return failed class when MR has CI but also has an unknown status', () => { + it('returns info class for pending status', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineActive = true; + expect(vm.mergeButtonClass).toEqual(inActionClass); + }); + + it('returns failed class for failed status', () => { vm.mr.hasCI = true; expect(vm.mergeButtonClass).toEqual(failedClass); }); + }); - it('should return default class when MR has no pipeline', () => { - expect(vm.mergeButtonClass).toEqual(defaultClass); + describe('status icon', () => { + it('defaults to tick icon', () => { + expect(vm.iconClass).toEqual('success'); + }); + + it('shows tick for success status', () => { + vm.mr.pipeline = true; + expect(vm.iconClass).toEqual('success'); }); - it('should return in action class when pipeline is active', () => { + it('shows tick for pending status', () => { vm.mr.pipeline = {}; vm.mr.isPipelineActive = true; - expect(vm.mergeButtonClass).toEqual(inActionClass); + expect(vm.iconClass).toEqual('success'); }); - it('should return failed class when pipeline is failed', () => { - vm.mr.pipeline = {}; - vm.mr.isPipelineFailed = true; - expect(vm.mergeButtonClass).toEqual(failedClass); + it('shows x for failed status', () => { + vm.mr.hasCI = true; + expect(vm.iconClass).toEqual('failed'); + }); + + it('shows x for merge not allowed', () => { + vm.mr.hasCI = true; + expect(vm.iconClass).toEqual('failed'); }); }); @@ -150,72 +213,54 @@ describe('MRWidgetReadyToMerge', () => { describe('isMergeButtonDisabled', () => { it('should return false with initial data', () => { + vm.mr.isMergeAllowed = true; expect(vm.isMergeButtonDisabled).toBeFalsy(); }); it('should return true when there is no commit message', () => { + vm.mr.isMergeAllowed = true; vm.commitMessage = ''; expect(vm.isMergeButtonDisabled).toBeTruthy(); }); it('should return true if merge is not allowed', () => { + vm.mr.isMergeAllowed = false; vm.mr.onlyAllowMergeIfPipelineSucceeds = true; - vm.mr.isPipelineFailed = true; expect(vm.isMergeButtonDisabled).toBeTruthy(); }); - it('should return true when there vm instance is making request', () => { + it('should return true when the vm instance is making request', () => { + vm.mr.isMergeAllowed = true; vm.isMakingRequest = true; expect(vm.isMergeButtonDisabled).toBeTruthy(); }); }); - - describe('Remove source branch checkbox', () => { - describe('when user can merge but cannot delete branch', () => { - it('isRemoveSourceBranchButtonDisabled should be true', () => { - expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true); - }); - - it('should be disabled in the rendered output', () => { - const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); - expect(checkboxElement.getAttribute('disabled')).toBe('disabled'); - }); - }); - - describe('when user can merge and can delete branch', () => { - beforeEach(() => { - this.customVm = createComponent({ - mr: { canRemoveSourceBranch: true }, - }); - }); - - it('isRemoveSourceBranchButtonDisabled should be false', () => { - expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false); - }); - - it('should be enabled in rendered output', () => { - const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input'); - expect(checkboxElement.getAttribute('disabled')).toBeNull(); - }); - }); - }); }); describe('methods', () => { - describe('isMergeAllowed', () => { - it('should return false with initial data', () => { - expect(vm.isMergeAllowed()).toBeTruthy(); + describe('shouldShowMergeControls', () => { + it('should return false when an external pipeline is running and required to succeed', () => { + vm.mr.isMergeAllowed = false; + vm.mr.isPipelineActive = false; + expect(vm.shouldShowMergeControls()).toBeFalsy(); }); - it('should return false when MR is set only merge when pipeline succeeds', () => { - vm.mr.onlyAllowMergeIfPipelineSucceeds = true; - expect(vm.isMergeAllowed()).toBeTruthy(); + it('should return true when the build succeeded or build not required to succeed', () => { + vm.mr.isMergeAllowed = true; + vm.mr.isPipelineActive = false; + expect(vm.shouldShowMergeControls()).toBeTruthy(); }); - it('should return true true', () => { - vm.mr.onlyAllowMergeIfPipelineSucceeds = true; - vm.mr.isPipelineFailed = true; - expect(vm.isMergeAllowed()).toBeFalsy(); + it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => { + vm.mr.isMergeAllowed = false; + vm.mr.isPipelineActive = true; + expect(vm.shouldShowMergeControls()).toBeTruthy(); + }); + + it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => { + vm.mr.isMergeAllowed = true; + vm.mr.isPipelineActive = true; + expect(vm.shouldShowMergeControls()).toBeTruthy(); }); }); @@ -419,4 +464,54 @@ describe('MRWidgetReadyToMerge', () => { }); }); }); + + describe('Remove source branch checkbox', () => { + describe('when user can merge but cannot delete branch', () => { + it('isRemoveSourceBranchButtonDisabled should be true', () => { + expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true); + }); + + it('should be disabled in the rendered output', () => { + const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBe('disabled'); + }); + }); + + describe('when user can merge and can delete branch', () => { + beforeEach(() => { + this.customVm = createComponent({ + mr: { canRemoveSourceBranch: true }, + }); + }); + + it('isRemoveSourceBranchButtonDisabled should be false', () => { + expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false); + }); + + it('should be enabled in rendered output', () => { + const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBeNull(); + }); + }); + }); + + describe('Commit message area', () => { + it('when using merge commits, should show "Modify commit message" button', () => { + const customVm = createComponent({ + mr: { ffOnlyEnabled: false }, + }); + + expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeNull(); + expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined(); + }); + + it('when fast-forward merge is enabled, only show fast-forward message', () => { + const customVm = createComponent({ + mr: { ffOnlyEnabled: true }, + }); + + expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeDefined(); + expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeNull(); + }); + }); }); 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 669ee248bf1..e4324e91502 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -59,23 +59,15 @@ describe('mrWidgetOptions', () => { }); describe('shouldRenderPipelines', () => { - it('should return true for the initial data', () => { - expect(vm.shouldRenderPipelines).toBeTruthy(); - }); + it('should return true when hasCI is true', () => { + vm.mr.hasCI = true; - it('should return true when pipeline is empty but MR.hasCI is set to true', () => { - vm.mr.pipeline = {}; expect(vm.shouldRenderPipelines).toBeTruthy(); }); - it('should return true when pipeline available', () => { + it('should return false when hasCI is false', () => { vm.mr.hasCI = false; - expect(vm.shouldRenderPipelines).toBeTruthy(); - }); - it('should return false when there is no pipeline', () => { - vm.mr.pipeline = {}; - vm.mr.hasCI = false; expect(vm.shouldRenderPipelines).toBeFalsy(); }); }); @@ -232,29 +224,41 @@ describe('mrWidgetOptions', () => { describe('handleMounted', () => { it('should call required methods to do the initial kick-off', () => { spyOn(vm, 'initDeploymentsPolling'); - spyOn(vm, 'setFavicon'); + spyOn(vm, 'setFaviconHelper'); vm.handleMounted(); - expect(vm.setFavicon).toHaveBeenCalled(); + expect(vm.setFaviconHelper).toHaveBeenCalled(); expect(vm.initDeploymentsPolling).toHaveBeenCalled(); }); }); describe('setFavicon', () => { + let faviconElement; + + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + document.body.appendChild(favicon); + + faviconElement = document.getElementById('favicon'); + }); + + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); + it('should call setFavicon method', () => { - spyOn(gl.utils, 'setFavicon'); - vm.setFavicon(); + vm.setFaviconHelper(); - expect(gl.utils.setFavicon).toHaveBeenCalledWith(vm.mr.ciStatusFaviconPath); + expect(faviconElement.getAttribute('href')).toEqual(vm.mr.ciStatusFaviconPath); }); it('should not call setFavicon when there is no ciStatusFaviconPath', () => { - spyOn(gl.utils, 'setFavicon'); vm.mr.ciStatusFaviconPath = null; - vm.setFavicon(); + vm.setFaviconHelper(); - expect(gl.utils.setFavicon).not.toHaveBeenCalled(); + expect(faviconElement.getAttribute('href')).toEqual(null); }); }); diff --git a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js index b63633c03b8..e667b4b3677 100644 --- a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js +++ b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js @@ -31,6 +31,7 @@ describe('MRWidgetService', () => { }); it('should have methods defined', () => { + window.history.pushState({}, null, '/'); const service = new MRWidgetService(mr); expect(service.merge()).toBeDefined(); diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js index 56dd0198ae2..8e5614b20f0 100644 --- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js +++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js @@ -18,5 +18,39 @@ describe('MergeRequestStore', () => { store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress }); expect(store.hasSHAChanged).toBe(false); }); + + describe('isPipelinePassing', () => { + it('is true when the CI status is `success`', () => { + store.setData({ ...mockData, ci_status: 'success' }); + expect(store.isPipelinePassing).toBe(true); + }); + + it('is true when the CI status is `success_with_warnings`', () => { + store.setData({ ...mockData, ci_status: 'success_with_warnings' }); + expect(store.isPipelinePassing).toBe(true); + }); + + it('is false when the CI status is `failed`', () => { + store.setData({ ...mockData, ci_status: 'failed' }); + expect(store.isPipelinePassing).toBe(false); + }); + + it('is false when the CI status is anything except `success`', () => { + store.setData({ ...mockData, ci_status: 'foobarbaz' }); + expect(store.isPipelinePassing).toBe(false); + }); + }); + + describe('isPipelineSkipped', () => { + it('should set isPipelineSkipped=true when the CI status is `skipped`', () => { + store.setData({ ...mockData, ci_status: 'skipped' }); + expect(store.isPipelineSkipped).toBe(true); + }); + + it('should set isPipelineSkipped=false when the CI status is anything except `skipped`', () => { + store.setData({ ...mockData, ci_status: 'foobarbaz' }); + expect(store.isPipelineSkipped).toBe(false); + }); + }); }); }); diff --git a/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js deleted file mode 100644 index 6df08f3ebe7..00000000000 --- a/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js +++ /dev/null @@ -1,20 +0,0 @@ -import Vue from 'vue'; -import confidentialIssue from '~/vue_shared/components/issue/confidential_issue_warning.vue'; - -describe('Confidential Issue Warning Component', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(confidentialIssue); - vm = new Component().$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render confidential issue warning information', () => { - expect(vm.$el.querySelector('i').className).toEqual('fa fa-eye-slash'); - expect(vm.$el.querySelector('span').textContent.trim()).toEqual('This is a confidential issue. Your comment will not be visible to the public.'); - }); -}); diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js new file mode 100644 index 00000000000..2cf4d8e00ed --- /dev/null +++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js @@ -0,0 +1,46 @@ +import Vue from 'vue'; +import issueWarning from '~/vue_shared/components/issue/issue_warning.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +const IssueWarning = Vue.extend(issueWarning); + +function formatWarning(string) { + // Replace newlines with a space then replace multiple spaces with one space + return string.trim().replace(/\n/g, ' ').replace(/\s\s+/g, ' '); +} + +describe('Issue Warning Component', () => { + describe('isLocked', () => { + it('should render locked issue warning information', () => { + const vm = mountComponent(IssueWarning, { + isLocked: true, + }); + + expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-lock'); + expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is locked. Only project members can comment.'); + }); + }); + + describe('isConfidential', () => { + it('should render confidential issue warning information', () => { + const vm = mountComponent(IssueWarning, { + isConfidential: true, + }); + + expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-eye-slash'); + expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This is a confidential issue. Your comment will not be visible to the public.'); + }); + }); + + describe('isLocked and isConfidential', () => { + it('should render locked and confidential issue warning information', () => { + const vm = mountComponent(IssueWarning, { + isLocked: true, + isConfidential: true, + }); + + expect(vm.$el.querySelector('i')).toBeFalsy(); + expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is confidential and locked. People without permission will never get a notification and won\'t be able to comment.'); + }); + }); +}); |