diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-08 09:08:23 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-08 09:08:23 +0000 |
commit | b001207ce2033589373cd7558ca69c4e5732ce6b (patch) | |
tree | 11f0b257a3a8e4644afd8995ac628a869d6d3811 /spec | |
parent | 449287e15a3633f7a17533c2da0273e31ddf1c1f (diff) | |
download | gitlab-ce-b001207ce2033589373cd7558ca69c4e5732ce6b.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/fixtures/api/schemas/public_api/v4/board.json | 3 | ||||
-rw-r--r-- | spec/frontend/awards_handler_spec.js (renamed from spec/javascripts/awards_handler_spec.js) | 111 | ||||
-rw-r--r-- | spec/frontend/collapsed_sidebar_todo_spec.js (renamed from spec/javascripts/collapsed_sidebar_todo_spec.js) | 21 | ||||
-rw-r--r-- | spec/frontend/comment_type_toggle_spec.js | 169 | ||||
-rw-r--r-- | spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js (renamed from spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js) | 36 | ||||
-rw-r--r-- | spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js (renamed from spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js) | 32 | ||||
-rw-r--r-- | spec/frontend/gl_form_spec.js (renamed from spec/javascripts/gl_form_spec.js) | 63 | ||||
-rw-r--r-- | spec/frontend/global_search_input_spec.js (renamed from spec/javascripts/global_search_input_spec.js) | 48 | ||||
-rw-r--r-- | spec/frontend/labels_issue_sidebar_spec.js (renamed from spec/javascripts/labels_issue_sidebar_spec.js) | 10 | ||||
-rw-r--r-- | spec/frontend/line_highlighter_spec.js | 268 | ||||
-rw-r--r-- | spec/frontend/merge_request_tabs_spec.js (renamed from spec/javascripts/merge_request_tabs_spec.js) | 161 | ||||
-rw-r--r-- | spec/frontend/right_sidebar_spec.js (renamed from spec/javascripts/right_sidebar_spec.js) | 22 | ||||
-rw-r--r-- | spec/frontend/shortcuts_spec.js | 46 | ||||
-rw-r--r-- | spec/frontend/user_popovers_spec.js (renamed from spec/javascripts/user_popovers_spec.js) | 10 | ||||
-rw-r--r-- | spec/frontend/zen_mode_spec.js (renamed from spec/javascripts/zen_mode_spec.js) | 18 | ||||
-rw-r--r-- | spec/javascripts/comment_type_toggle_spec.js | 168 | ||||
-rw-r--r-- | spec/javascripts/line_highlighter_spec.js | 261 | ||||
-rw-r--r-- | spec/javascripts/shortcuts_spec.js | 46 | ||||
-rw-r--r-- | spec/lib/gitlab/routing_spec.rb | 21 | ||||
-rw-r--r-- | spec/models/project_spec.rb | 26 |
20 files changed, 815 insertions, 725 deletions
diff --git a/spec/fixtures/api/schemas/public_api/v4/board.json b/spec/fixtures/api/schemas/public_api/v4/board.json index e4933ee0b93..89a21c29969 100644 --- a/spec/fixtures/api/schemas/public_api/v4/board.json +++ b/spec/fixtures/api/schemas/public_api/v4/board.json @@ -78,7 +78,8 @@ }, "position": { "type": ["integer", "null"] }, "max_issue_count": { "type": "integer" }, - "max_issue_weight": { "type": "integer" } + "max_issue_weight": { "type": "integer" }, + "limit_metric": { "type": ["string", "null"] } }, "additionalProperties": false } diff --git a/spec/javascripts/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js index 02200f77ad7..754f0702b84 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/frontend/awards_handler_spec.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; import loadAwardsHandler from '~/awards_handler'; import '~/lib/utils/common_utils'; +import waitForPromises from './helpers/wait_for_promises'; window.gl = window.gl || {}; window.gon = window.gon || {}; @@ -10,28 +11,32 @@ let openAndWaitForEmojiMenu; let awardsHandler = null; const urlRoot = gon.relative_url_root; -const lazyAssert = function(done, assertFn) { - setTimeout(function() { - assertFn(); - done(); - // Maybe jasmine.clock here? - }, 333); +const lazyAssert = (done, assertFn) => { + jest.runOnlyPendingTimers(); + waitForPromises() + .then(() => { + assertFn(); + done(); + }) + .catch(e => { + throw e; + }); }; -describe('AwardsHandler', function() { +describe('AwardsHandler', () => { preloadFixtures('snippets/show.html'); - beforeEach(function(done) { + beforeEach(done => { loadFixtures('snippets/show.html'); loadAwardsHandler(true) .then(obj => { awardsHandler = obj; - spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); + jest.spyOn(awardsHandler, 'postEmoji').mockImplementation((button, url, emoji, cb) => cb()); done(); }) - .catch(fail); + .catch(done.fail); let isEmojiMenuBuilt = false; - openAndWaitForEmojiMenu = function() { + openAndWaitForEmojiMenu = () => { return new Promise(resolve => { if (isEmojiMenuBuilt) { resolve(); @@ -49,7 +54,7 @@ describe('AwardsHandler', function() { }; }); - afterEach(function() { + afterEach(() => { // restore original url root value gon.relative_url_root = urlRoot; @@ -59,12 +64,12 @@ describe('AwardsHandler', function() { awardsHandler.destroy(); }); - describe('::showEmojiMenu', function() { - it('should show emoji menu when Add emoji button clicked', function(done) { + describe('::showEmojiMenu', () => { + it('should show emoji menu when Add emoji button clicked', done => { $('.js-add-award') .eq(0) .click(); - lazyAssert(done, function() { + lazyAssert(done, () => { const $emojiMenu = $('.emoji-menu'); expect($emojiMenu.length).toBe(1); @@ -74,20 +79,20 @@ describe('AwardsHandler', function() { }); }); - it('should also show emoji menu for the smiley icon in notes', function(done) { + it('should also show emoji menu for the smiley icon in notes', done => { $('.js-add-award.note-action-button').click(); - lazyAssert(done, function() { + lazyAssert(done, () => { const $emojiMenu = $('.emoji-menu'); expect($emojiMenu.length).toBe(1); }); }); - it('should remove emoji menu when body is clicked', function(done) { + it('should remove emoji menu when body is clicked', done => { $('.js-add-award') .eq(0) .click(); - lazyAssert(done, function() { + lazyAssert(done, () => { const $emojiMenu = $('.emoji-menu'); $('body').click(); @@ -97,11 +102,11 @@ describe('AwardsHandler', function() { }); }); - it('should not remove emoji menu when search is clicked', function(done) { + it('should not remove emoji menu when search is clicked', done => { $('.js-add-award') .eq(0) .click(); - lazyAssert(done, function() { + lazyAssert(done, () => { const $emojiMenu = $('.emoji-menu'); $('.emoji-search').click(); @@ -112,8 +117,8 @@ describe('AwardsHandler', function() { }); }); - describe('::addAwardToEmojiBar', function() { - it('should add emoji to votes block', function() { + describe('::addAwardToEmojiBar', () => { + it('should add emoji to votes block', () => { const $votesBlock = $('.js-awards-block').eq(0); awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); const $emojiButton = $votesBlock.find('[data-name=heart]'); @@ -123,7 +128,7 @@ describe('AwardsHandler', function() { expect($votesBlock.hasClass('hidden')).toBe(false); }); - it('should remove the emoji when we click again', function() { + it('should remove the emoji when we click again', () => { const $votesBlock = $('.js-awards-block').eq(0); awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); @@ -132,7 +137,7 @@ describe('AwardsHandler', function() { expect($emojiButton.length).toBe(0); }); - it('should decrement the emoji counter', function() { + it('should decrement the emoji counter', () => { const $votesBlock = $('.js-awards-block').eq(0); awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); const $emojiButton = $votesBlock.find('[data-name=heart]'); @@ -144,8 +149,8 @@ describe('AwardsHandler', function() { }); }); - describe('::userAuthored', function() { - it('should update tooltip to user authored title', function() { + describe('::userAuthored', () => { + it('should update tooltip to user authored title', () => { const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); $thumbsUpEmoji.attr('data-title', 'sam'); @@ -156,27 +161,25 @@ describe('AwardsHandler', function() { ); }); - it('should restore tooltip back to initial vote list', function() { - jasmine.clock().install(); + it('should restore tooltip back to initial vote list', () => { const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); $thumbsUpEmoji.attr('data-title', 'sam'); awardsHandler.userAuthored($thumbsUpEmoji); - jasmine.clock().tick(2801); - jasmine.clock().uninstall(); + jest.advanceTimersByTime(2801); expect($thumbsUpEmoji.data('originalTitle')).toBe('sam'); }); }); - describe('::getAwardUrl', function() { - it('returns the url for request', function() { + describe('::getAwardUrl', () => { + it('returns the url for request', () => { expect(awardsHandler.getAwardUrl()).toBe('http://test.host/snippets/1/toggle_award_emoji'); }); }); - describe('::addAward and ::checkMutuality', function() { - it('should handle :+1: and :-1: mutuality', function() { + describe('::addAward and ::checkMutuality', () => { + it('should handle :+1: and :-1: mutuality', () => { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); @@ -194,8 +197,8 @@ describe('AwardsHandler', function() { }); }); - describe('::removeEmoji', function() { - it('should remove emoji', function() { + describe('::removeEmoji', () => { + it('should remove emoji', () => { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); awardsHandler.addAward($votesBlock, awardUrl, 'fire', false); @@ -207,8 +210,8 @@ describe('AwardsHandler', function() { }); }); - describe('::addYouToUserList', function() { - it('should prepend "You" to the award tooltip', function() { + describe('::addYouToUserList', () => { + it('should prepend "You" to the award tooltip', () => { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); @@ -219,7 +222,7 @@ describe('AwardsHandler', function() { expect($thumbsUpEmoji.data('originalTitle')).toBe('You, sam, jerry, max, and andy'); }); - it('handles the special case where "You" is not cleanly comma separated', function() { + it('handles the special case where "You" is not cleanly comma separated', () => { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); @@ -231,8 +234,8 @@ describe('AwardsHandler', function() { }); }); - describe('::removeYouToUserList', function() { - it('removes "You" from the front of the tooltip', function() { + describe('::removeYouToUserList', () => { + it('removes "You" from the front of the tooltip', () => { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); @@ -244,7 +247,7 @@ describe('AwardsHandler', function() { expect($thumbsUpEmoji.data('originalTitle')).toBe('sam, jerry, max, and andy'); }); - it('handles the special case where "You" is not cleanly comma separated', function() { + it('handles the special case where "You" is not cleanly comma separated', () => { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); @@ -258,7 +261,7 @@ describe('AwardsHandler', function() { }); describe('::searchEmojis', () => { - it('should filter the emoji', function(done) { + it('should filter the emoji', done => { openAndWaitForEmojiMenu() .then(() => { expect($('[data-name=angel]').is(':visible')).toBe(true); @@ -276,7 +279,7 @@ describe('AwardsHandler', function() { }); }); - it('should clear the search when searching for nothing', function(done) { + it('should clear the search when searching for nothing', done => { openAndWaitForEmojiMenu() .then(() => { awardsHandler.searchEmojis('ali'); @@ -298,9 +301,9 @@ describe('AwardsHandler', function() { }); }); - describe('emoji menu', function() { + describe('emoji menu', () => { const emojiSelector = '[data-name="sunglasses"]'; - const openEmojiMenuAndAddEmoji = function() { + const openEmojiMenuAndAddEmoji = () => { return openAndWaitForEmojiMenu().then(() => { const $menu = $('.emoji-menu'); const $block = $('.js-awards-block'); @@ -315,7 +318,7 @@ describe('AwardsHandler', function() { }); }; - it('should add selected emoji to awards block', function(done) { + it('should add selected emoji to awards block', done => { openEmojiMenuAndAddEmoji() .then(done) .catch(err => { @@ -323,7 +326,7 @@ describe('AwardsHandler', function() { }); }); - it('should remove already selected emoji', function(done) { + it('should remove already selected emoji', done => { openEmojiMenuAndAddEmoji() .then(() => { $('.js-add-award') @@ -344,13 +347,13 @@ describe('AwardsHandler', function() { }); }); - describe('frequently used emojis', function() { + describe('frequently used emojis', () => { beforeEach(() => { // Clear it out Cookies.set('frequently_used_emojis', ''); }); - it('shouldn\'t have any "Frequently used" heading if no frequently used emojis', function(done) { + it('shouldn\'t have any "Frequently used" heading if no frequently used emojis', done => { return openAndWaitForEmojiMenu() .then(() => { const emojiMenu = document.querySelector('.emoji-menu'); @@ -364,7 +367,7 @@ describe('AwardsHandler', function() { }); }); - it('should have any frequently used section when there are frequently used emojis', function(done) { + it('should have any frequently used section when there are frequently used emojis', done => { awardsHandler.addEmojiToFrequentlyUsedList('8ball'); return openAndWaitForEmojiMenu() @@ -383,7 +386,7 @@ describe('AwardsHandler', function() { }); }); - it('should disregard invalid frequently used emoji that are being attempted to be added', function() { + it('should disregard invalid frequently used emoji that are being attempted to be added', () => { awardsHandler.addEmojiToFrequentlyUsedList('8ball'); awardsHandler.addEmojiToFrequentlyUsedList('invalid_emoji'); awardsHandler.addEmojiToFrequentlyUsedList('grinning'); @@ -391,7 +394,7 @@ describe('AwardsHandler', function() { expect(awardsHandler.getFrequentlyUsedEmojis()).toEqual(['8ball', 'grinning']); }); - it('should disregard invalid frequently used emoji already set in cookie', function() { + it('should disregard invalid frequently used emoji already set in cookie', () => { Cookies.set('frequently_used_emojis', '8ball,invalid_emoji,grinning'); expect(awardsHandler.getFrequentlyUsedEmojis()).toEqual(['8ball', 'grinning']); diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/frontend/collapsed_sidebar_todo_spec.js index f2eb08fa198..0ea797ce4b3 100644 --- a/spec/javascripts/collapsed_sidebar_todo_spec.js +++ b/spec/frontend/collapsed_sidebar_todo_spec.js @@ -3,7 +3,8 @@ import { clone } from 'lodash'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import Sidebar from '~/right_sidebar'; -import timeoutPromise from './helpers/set_timeout_promise_helper'; +import waitForPromises from './helpers/wait_for_promises'; +import { TEST_HOST } from 'spec/test_constants'; describe('Issuable right sidebar collapsed todo toggle', () => { const fixtureName = 'issues/open-issue.html'; @@ -23,7 +24,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { mock = new MockAdapter(axios); - mock.onPost(`${gl.TEST_HOST}/frontend-fixtures/issues-project/todos`).reply(() => { + mock.onPost(`${TEST_HOST}/frontend-fixtures/issues-project/todos`).reply(() => { const response = clone(todoData); return [200, response]; @@ -64,7 +65,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { it('toggle todo state', done => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - setTimeout(() => { + setImmediate(() => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), ).not.toBeNull(); @@ -82,7 +83,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { it('toggle todo state of expanded todo toggle', done => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - setTimeout(() => { + setImmediate(() => { expect( document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), ).toBe('Mark as done'); @@ -94,7 +95,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { it('toggles todo button tooltip', done => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - setTimeout(() => { + setImmediate(() => { expect( document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') @@ -108,7 +109,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { it('marks todo as done', done => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - timeoutPromise() + waitForPromises() .then(() => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), @@ -116,7 +117,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); }) - .then(timeoutPromise) + .then(waitForPromises) .then(() => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), @@ -133,7 +134,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { it('updates aria-label to Mark as done', done => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - setTimeout(() => { + setImmediate(() => { expect( document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') @@ -147,7 +148,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { it('updates aria-label to add todo', done => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - timeoutPromise() + waitForPromises() .then(() => { expect( document @@ -157,7 +158,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); }) - .then(timeoutPromise) + .then(waitForPromises) .then(() => { expect( document diff --git a/spec/frontend/comment_type_toggle_spec.js b/spec/frontend/comment_type_toggle_spec.js new file mode 100644 index 00000000000..06dbfac1803 --- /dev/null +++ b/spec/frontend/comment_type_toggle_spec.js @@ -0,0 +1,169 @@ +import CommentTypeToggle from '~/comment_type_toggle'; +import DropLab from '~/droplab/drop_lab'; +import InputSetter from '~/droplab/plugins/input_setter'; + +describe('CommentTypeToggle', () => { + const testContext = {}; + + describe('class constructor', () => { + beforeEach(() => { + testContext.dropdownTrigger = {}; + testContext.dropdownList = {}; + testContext.noteTypeInput = {}; + testContext.submitButton = {}; + testContext.closeButton = {}; + + testContext.commentTypeToggle = new CommentTypeToggle({ + dropdownTrigger: testContext.dropdownTrigger, + dropdownList: testContext.dropdownList, + noteTypeInput: testContext.noteTypeInput, + submitButton: testContext.submitButton, + closeButton: testContext.closeButton, + }); + }); + + it('should set .dropdownTrigger', () => { + expect(testContext.commentTypeToggle.dropdownTrigger).toBe(testContext.dropdownTrigger); + }); + + it('should set .dropdownList', () => { + expect(testContext.commentTypeToggle.dropdownList).toBe(testContext.dropdownList); + }); + + it('should set .noteTypeInput', () => { + expect(testContext.commentTypeToggle.noteTypeInput).toBe(testContext.noteTypeInput); + }); + + it('should set .submitButton', () => { + expect(testContext.commentTypeToggle.submitButton).toBe(testContext.submitButton); + }); + + it('should set .closeButton', () => { + expect(testContext.commentTypeToggle.closeButton).toBe(testContext.closeButton); + }); + + it('should set .reopenButton', () => { + expect(testContext.commentTypeToggle.reopenButton).toBe(testContext.reopenButton); + }); + }); + + describe('initDroplab', () => { + beforeEach(() => { + testContext.commentTypeToggle = { + dropdownTrigger: {}, + dropdownList: {}, + noteTypeInput: {}, + submitButton: {}, + closeButton: {}, + setConfig: () => {}, + }; + testContext.config = {}; + + jest.spyOn(DropLab.prototype, 'init').mockImplementation(); + jest.spyOn(DropLab.prototype, 'constructor').mockImplementation(); + + jest.spyOn(testContext.commentTypeToggle, 'setConfig').mockReturnValue(testContext.config); + + CommentTypeToggle.prototype.initDroplab.call(testContext.commentTypeToggle); + }); + + it('should instantiate a DropLab instance and set .droplab', () => { + expect(testContext.commentTypeToggle.droplab instanceof DropLab).toBe(true); + }); + + it('should call .setConfig', () => { + expect(testContext.commentTypeToggle.setConfig).toHaveBeenCalled(); + }); + + it('should call DropLab.prototype.init', () => { + expect(DropLab.prototype.init).toHaveBeenCalledWith( + testContext.commentTypeToggle.dropdownTrigger, + testContext.commentTypeToggle.dropdownList, + [InputSetter], + testContext.config, + ); + }); + }); + + describe('setConfig', () => { + describe('if no .closeButton is provided', () => { + beforeEach(() => { + testContext.commentTypeToggle = { + dropdownTrigger: {}, + dropdownList: {}, + noteTypeInput: {}, + submitButton: {}, + reopenButton: {}, + }; + + testContext.setConfig = CommentTypeToggle.prototype.setConfig.call( + testContext.commentTypeToggle, + ); + }); + + it('should not add .closeButton related InputSetter config', () => { + expect(testContext.setConfig).toEqual({ + InputSetter: [ + { + input: testContext.commentTypeToggle.noteTypeInput, + valueAttribute: 'data-value', + }, + { + input: testContext.commentTypeToggle.submitButton, + valueAttribute: 'data-submit-text', + }, + { + input: testContext.commentTypeToggle.reopenButton, + valueAttribute: 'data-reopen-text', + }, + { + input: testContext.commentTypeToggle.reopenButton, + valueAttribute: 'data-reopen-text', + inputAttribute: 'data-alternative-text', + }, + ], + }); + }); + }); + + describe('if no .reopenButton is provided', () => { + beforeEach(() => { + testContext.commentTypeToggle = { + dropdownTrigger: {}, + dropdownList: {}, + noteTypeInput: {}, + submitButton: {}, + closeButton: {}, + }; + + testContext.setConfig = CommentTypeToggle.prototype.setConfig.call( + testContext.commentTypeToggle, + ); + }); + + it('should not add .reopenButton related InputSetter config', () => { + expect(testContext.setConfig).toEqual({ + InputSetter: [ + { + input: testContext.commentTypeToggle.noteTypeInput, + valueAttribute: 'data-value', + }, + { + input: testContext.commentTypeToggle.submitButton, + valueAttribute: 'data-submit-text', + }, + { + input: testContext.commentTypeToggle.closeButton, + valueAttribute: 'data-close-text', + }, + { + input: testContext.commentTypeToggle.closeButton, + valueAttribute: 'data-close-text', + inputAttribute: 'data-alternative-text', + }, + ], + }); + }); + }); + }); +}); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js index 853f6b3b7b8..e9ee69ca163 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js @@ -1,9 +1,13 @@ -import $ from 'jquery'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager'; describe('Filtered Search Dropdown Manager', () => { + let mock; + beforeEach(() => { - spyOn($, 'ajax'); + mock = new MockAdapter(axios); + mock.onGet().reply(200); }); describe('addWordToInput', () => { @@ -32,7 +36,7 @@ describe('Filtered Search Dropdown Manager', () => { const token = document.querySelector('.tokens-container .js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('milestone'); + expect(token.querySelector('.name').textContent).toBe('milestone'); expect(getInputValue()).toBe(''); }); @@ -42,7 +46,7 @@ describe('Filtered Search Dropdown Manager', () => { let token = document.querySelector('.tokens-container .js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('label'); + expect(token.querySelector('.name').textContent).toBe('label'); expect(getInputValue()).toBe(''); FilteredSearchDropdownManager.addWordToInput({ tokenName: 'label', tokenOperator: '=' }); @@ -50,8 +54,8 @@ describe('Filtered Search Dropdown Manager', () => { token = document.querySelector('.tokens-container .js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('label'); - expect(token.querySelector('.operator').innerText).toBe('='); + expect(token.querySelector('.name').textContent).toBe('label'); + expect(token.querySelector('.operator').textContent).toBe('='); expect(getInputValue()).toBe(''); FilteredSearchDropdownManager.addWordToInput({ @@ -64,9 +68,9 @@ describe('Filtered Search Dropdown Manager', () => { token = document.querySelector('.tokens-container .js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('label'); - expect(token.querySelector('.operator').innerText).toBe('='); - expect(token.querySelector('.value').innerText).toBe('none'); + expect(token.querySelector('.name').textContent).toBe('label'); + expect(token.querySelector('.operator').textContent).toBe('='); + expect(token.querySelector('.value').textContent).toBe('none'); expect(getInputValue()).toBe(''); }); }); @@ -79,7 +83,7 @@ describe('Filtered Search Dropdown Manager', () => { const token = document.querySelector('.tokens-container .js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('author'); + expect(token.querySelector('.name').textContent).toBe('author'); expect(getInputValue()).toBe(''); }); @@ -97,9 +101,9 @@ describe('Filtered Search Dropdown Manager', () => { const token = document.querySelector('.tokens-container .js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('author'); - expect(token.querySelector('.operator').innerText).toBe('='); - expect(token.querySelector('.value').innerText).toBe('@root'); + expect(token.querySelector('.name').textContent).toBe('author'); + expect(token.querySelector('.operator').textContent).toBe('='); + expect(token.querySelector('.value').textContent).toBe('@root'); expect(getInputValue()).toBe(''); }); @@ -116,9 +120,9 @@ describe('Filtered Search Dropdown Manager', () => { const token = document.querySelector('.tokens-container .js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); - expect(token.querySelector('.name').innerText).toBe('label'); - expect(token.querySelector('.operator').innerText).toBe('='); - expect(token.querySelector('.value').innerText).toBe('~\'"test me"\''); + expect(token.querySelector('.name').textContent).toBe('label'); + expect(token.querySelector('.operator').textContent).toBe('='); + expect(token.querySelector('.value').textContent).toBe('~\'"test me"\''); expect(getInputValue()).toBe(''); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js index fda078bd41c..e59ee925cc7 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js @@ -1,7 +1,10 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Filtered Search Visual Tokens', () => { + let mock; const subject = FilteredSearchVisualTokens; const findElements = tokenElement => { @@ -17,6 +20,9 @@ describe('Filtered Search Visual Tokens', () => { let bugLabelToken; beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet().reply(200); + setFixtures(` <ul class="tokens-container"> ${FilteredSearchSpecHelper.createInputHTML()} @@ -248,15 +254,15 @@ describe('Filtered Search Visual Tokens', () => { }); it('contains name div', () => { - expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything()); + expect(tokenElement.querySelector('.name')).toEqual(expect.anything()); }); it('contains value container div', () => { - expect(tokenElement.querySelector('.value-container')).toEqual(jasmine.anything()); + expect(tokenElement.querySelector('.value-container')).toEqual(expect.anything()); }); it('contains value div', () => { - expect(tokenElement.querySelector('.value-container .value')).toEqual(jasmine.anything()); + expect(tokenElement.querySelector('.value-container .value')).toEqual(expect.anything()); }); it('contains selectable class', () => { @@ -270,12 +276,12 @@ describe('Filtered Search Visual Tokens', () => { describe('remove token', () => { it('contains remove-token button', () => { expect(tokenElement.querySelector('.value-container .remove-token')).toEqual( - jasmine.anything(), + expect.anything(), ); }); it('contains fa-close icon', () => { - expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(jasmine.anything()); + expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(expect.anything()); }); }); }); @@ -453,7 +459,7 @@ describe('Filtered Search Visual Tokens', () => { valueContainer.dataset.originalValue = originalValue; const avatar = document.createElement('img'); const valueElement = valueContainer.querySelector('.value'); - valueElement.insertAdjacentElement('afterbegin', avatar); + valueElement.appendChild(avatar); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( authorToken.outerHTML, ); @@ -573,7 +579,7 @@ describe('Filtered Search Visual Tokens', () => { it("tokenize's existing input", () => { input.value = 'some text'; - spyOn(subject, 'tokenizeInput').and.callThrough(); + jest.spyOn(subject, 'tokenizeInput'); subject.editToken(token); @@ -635,8 +641,8 @@ describe('Filtered Search Visual Tokens', () => { FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', 'none'), ); - spyOn(subject, 'tokenizeInput').and.callFake(() => {}); - spyOn(subject, 'getLastVisualTokenBeforeInput').and.callThrough(); + jest.spyOn(subject, 'tokenizeInput').mockImplementation(() => {}); + jest.spyOn(subject, 'getLastVisualTokenBeforeInput'); subject.moveInputToTheRight(); @@ -711,12 +717,16 @@ describe('Filtered Search Visual Tokens', () => { it('renders a author token value element', () => { const { tokenNameElement, tokenValueElement } = findElements(authorToken); - const tokenName = tokenNameElement.innerText; + const tokenName = tokenNameElement.textContent; const tokenValue = 'new value'; subject.renderVisualTokenValue(authorToken, tokenName, tokenValue); - expect(tokenValueElement.innerText).toBe(tokenValue); + jest.runOnlyPendingTimers(); + + setImmediate(() => { + expect(tokenValueElement.textContent).toBe(tokenValue); + }); }); }); }); diff --git a/spec/javascripts/gl_form_spec.js b/spec/frontend/gl_form_spec.js index 69b3dae743a..150d8a053d5 100644 --- a/spec/javascripts/gl_form_spec.js +++ b/spec/frontend/gl_form_spec.js @@ -5,51 +5,56 @@ import '~/lib/utils/text_utility'; import '~/lib/utils/common_utils'; describe('GLForm', () => { - describe('when instantiated', function() { + const testContext = {}; + + describe('when instantiated', () => { beforeEach(done => { - this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>'); - this.textarea = this.form.find('textarea'); - spyOn($.prototype, 'off').and.returnValue(this.textarea); - spyOn($.prototype, 'on').and.returnValue(this.textarea); - spyOn($.prototype, 'css'); - - this.glForm = new GLForm(this.form, false); - setTimeout(() => { - $.prototype.off.calls.reset(); - $.prototype.on.calls.reset(); - $.prototype.css.calls.reset(); + testContext.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>'); + testContext.textarea = testContext.form.find('textarea'); + jest.spyOn($.prototype, 'off').mockReturnValue(testContext.textarea); + jest.spyOn($.prototype, 'on').mockReturnValue(testContext.textarea); + jest.spyOn($.prototype, 'css').mockImplementation(() => {}); + + testContext.glForm = new GLForm(testContext.form, false); + + setImmediate(() => { + $.prototype.off.mockClear(); + $.prototype.on.mockClear(); + $.prototype.css.mockClear(); done(); }); }); describe('setupAutosize', () => { beforeEach(done => { - this.glForm.setupAutosize(); - setTimeout(() => { + testContext.glForm.setupAutosize(); + + setImmediate(() => { done(); }); }); it('should register an autosize event handler on the textarea', () => { expect($.prototype.off).toHaveBeenCalledWith('autosize:resized'); - expect($.prototype.on).toHaveBeenCalledWith('autosize:resized', jasmine.any(Function)); + expect($.prototype.on).toHaveBeenCalledWith('autosize:resized', expect.any(Function)); }); it('should register a mouseup event handler on the textarea', () => { expect($.prototype.off).toHaveBeenCalledWith('mouseup.autosize'); - expect($.prototype.on).toHaveBeenCalledWith('mouseup.autosize', jasmine.any(Function)); + expect($.prototype.on).toHaveBeenCalledWith('mouseup.autosize', expect.any(Function)); }); it('should set the resize css property to vertical', () => { + jest.runOnlyPendingTimers(); expect($.prototype.css).toHaveBeenCalledWith('resize', 'vertical'); }); }); describe('setHeightData', () => { beforeEach(() => { - spyOn($.prototype, 'data'); - spyOn($.prototype, 'outerHeight').and.returnValue(200); - this.glForm.setHeightData(); + jest.spyOn($.prototype, 'data').mockImplementation(() => {}); + jest.spyOn($.prototype, 'outerHeight').mockReturnValue(200); + testContext.glForm.setHeightData(); }); it('should set the height data attribute', () => { @@ -64,12 +69,12 @@ describe('GLForm', () => { describe('destroyAutosize', () => { describe('when called', () => { beforeEach(() => { - spyOn($.prototype, 'data'); - spyOn($.prototype, 'outerHeight').and.returnValue(200); - spyOn(window, 'outerHeight').and.returnValue(400); - spyOn(autosize, 'destroy'); + jest.spyOn($.prototype, 'data').mockImplementation(() => {}); + jest.spyOn($.prototype, 'outerHeight').mockReturnValue(200); + window.outerHeight = () => 400; + jest.spyOn(autosize, 'destroy').mockImplementation(() => {}); - this.glForm.destroyAutosize(); + testContext.glForm.destroyAutosize(); }); it('should call outerHeight', () => { @@ -81,7 +86,7 @@ describe('GLForm', () => { }); it('should call autosize destroy', () => { - expect(autosize.destroy).toHaveBeenCalledWith(this.textarea); + expect(autosize.destroy).toHaveBeenCalledWith(testContext.textarea); }); it('should set the data-height attribute', () => { @@ -98,11 +103,11 @@ describe('GLForm', () => { }); it('should return undefined if the data-height equals the outerHeight', () => { - spyOn($.prototype, 'outerHeight').and.returnValue(200); - spyOn($.prototype, 'data').and.returnValue(200); - spyOn(autosize, 'destroy'); + jest.spyOn($.prototype, 'outerHeight').mockReturnValue(200); + jest.spyOn($.prototype, 'data').mockReturnValue(200); + jest.spyOn(autosize, 'destroy').mockImplementation(() => {}); - expect(this.glForm.destroyAutosize()).toBeUndefined(); + expect(testContext.glForm.destroyAutosize()).toBeUndefined(); expect(autosize.destroy).not.toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/global_search_input_spec.js b/spec/frontend/global_search_input_spec.js index 00ae8a8f2ea..8c00ea5f193 100644 --- a/spec/javascripts/global_search_input_spec.js +++ b/spec/frontend/global_search_input_spec.js @@ -28,7 +28,7 @@ describe('Global search input dropdown', () => { const groupName = 'Gitlab Org'; - const removeBodyAttributes = function() { + const removeBodyAttributes = () => { const $body = $('body'); $body.removeAttr('data-page'); @@ -38,7 +38,7 @@ describe('Global search input dropdown', () => { // Add required attributes to body before starting the test. // section would be dashboard|group|project - const addBodyAttributes = function(section) { + const addBodyAttributes = section => { if (section == null) { section = 'dashboard'; } @@ -57,12 +57,12 @@ describe('Global search input dropdown', () => { } }; - const disableProjectIssues = function() { + const disableProjectIssues = () => { document.querySelector('.js-search-project-options').setAttribute('data-issues-disabled', true); }; // Mock `gl` object in window for dashboard specific page. App code will need it. - const mockDashboardOptions = function() { + const mockDashboardOptions = () => { window.gl || (window.gl = {}); return (window.gl.dashboardOptions = { issuesPath: dashboardIssuesPath, @@ -71,7 +71,7 @@ describe('Global search input dropdown', () => { }; // Mock `gl` object in window for project specific page. App code will need it. - const mockProjectOptions = function() { + const mockProjectOptions = () => { window.gl || (window.gl = {}); return (window.gl.projectOptions = { 'gitlab-ce': { @@ -82,7 +82,7 @@ describe('Global search input dropdown', () => { }); }; - const mockGroupOptions = function() { + const mockGroupOptions = () => { window.gl || (window.gl = {}); return (window.gl.groupOptions = { 'gitlab-org': { @@ -93,7 +93,7 @@ describe('Global search input dropdown', () => { }); }; - const assertLinks = function(list, issuesPath, mrsPath) { + const assertLinks = (list, issuesPath, mrsPath) => { if (issuesPath) { const issuesAssignedToMeLink = `a[href="${issuesPath}/?assignee_username=${userName}"]`; const issuesIHaveCreatedLink = `a[href="${issuesPath}/?author_username=${userName}"]`; @@ -113,7 +113,7 @@ describe('Global search input dropdown', () => { }; preloadFixtures('static/global_search_input.html'); - beforeEach(function() { + beforeEach(() => { loadFixtures('static/global_search_input.html'); window.gon = {}; @@ -123,13 +123,13 @@ describe('Global search input dropdown', () => { return (widget = initGlobalSearchInput()); }); - afterEach(function() { + afterEach(() => { // Undo what we did to the shared <body> removeBodyAttributes(); window.gon = {}; }); - it('should show Dashboard specific dropdown menu', function() { + it('should show Dashboard specific dropdown menu', () => { addBodyAttributes(); mockDashboardOptions(); widget.searchInput.triggerHandler('focus'); @@ -137,7 +137,7 @@ describe('Global search input dropdown', () => { return assertLinks(list, dashboardIssuesPath, dashboardMRsPath); }); - it('should show Group specific dropdown menu', function() { + it('should show Group specific dropdown menu', () => { addBodyAttributes('group'); mockGroupOptions(); widget.searchInput.triggerHandler('focus'); @@ -145,7 +145,7 @@ describe('Global search input dropdown', () => { return assertLinks(list, groupIssuesPath, groupMRsPath); }); - it('should show Project specific dropdown menu', function() { + it('should show Project specific dropdown menu', () => { addBodyAttributes('project'); mockProjectOptions(); widget.searchInput.triggerHandler('focus'); @@ -153,7 +153,7 @@ describe('Global search input dropdown', () => { return assertLinks(list, projectIssuesPath, projectMRsPath); }); - it('should show only Project mergeRequest dropdown menu items when project issues are disabled', function() { + it('should show only Project mergeRequest dropdown menu items when project issues are disabled', () => { addBodyAttributes('project'); disableProjectIssues(); mockProjectOptions(); @@ -162,7 +162,7 @@ describe('Global search input dropdown', () => { assertLinks(list, null, projectMRsPath); }); - it('should not show category related menu if there is text in the input', function() { + it('should not show category related menu if there is text in the input', () => { addBodyAttributes('project'); mockProjectOptions(); widget.searchInput.val('help'); @@ -173,12 +173,12 @@ describe('Global search input dropdown', () => { expect(list.find(link).length).toBe(0); }); - it('should not submit the search form when selecting an autocomplete row with the keyboard', function() { + it('should not submit the search form when selecting an autocomplete row with the keyboard', () => { const ENTER = 13; const DOWN = 40; addBodyAttributes(); mockDashboardOptions(true); - const submitSpy = spyOnEvent('form', 'submit'); + const submitSpy = jest.spyOn(document.querySelector('form'), 'submit'); widget.searchInput.triggerHandler('focus'); widget.wrap.trigger($.Event('keydown', { which: DOWN })); const enterKeyEvent = $.Event('keydown', { which: ENTER }); @@ -186,16 +186,16 @@ describe('Global search input dropdown', () => { // This does not currently catch failing behavior. For security reasons, // browsers will not trigger default behavior (form submit, in this // example) on JavaScript-created keypresses. - expect(submitSpy).not.toHaveBeenTriggered(); + expect(submitSpy).not.toHaveBeenCalled(); }); - describe('disableDropdown', function() { - beforeEach(function() { + describe('disableDropdown', () => { + beforeEach(() => { widget.enableDropdown(); }); - it('should close the Dropdown', function() { - const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown'); + it('should close the Dropdown', () => { + const toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown'); widget.dropdown.addClass('show'); widget.disableDropdown(); @@ -204,9 +204,9 @@ describe('Global search input dropdown', () => { }); }); - describe('enableDropdown', function() { - it('should open the Dropdown', function() { - const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown'); + describe('enableDropdown', () => { + it('should open the Dropdown', () => { + const toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown'); widget.enableDropdown(); expect(toggleSpy).toHaveBeenCalledWith('toggle'); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/frontend/labels_issue_sidebar_spec.js index 94e833ec83b..fafefca94df 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js +++ b/spec/frontend/labels_issue_sidebar_spec.js @@ -21,7 +21,9 @@ function testLabelClicks(labelOrder, done) { .get(0) .click(); - setTimeout(() => { + jest.runOnlyPendingTimers(); + + setImmediate(() => { const labelsInDropdown = $('.dropdown-content a'); expect(labelsInDropdown.length).toBe(10); @@ -38,11 +40,11 @@ function testLabelClicks(labelOrder, done) { .get(0) .click(); - setTimeout(() => { + setImmediate(() => { expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe(labelOrder); done(); - }, 0); - }, 0); + }); + }); } describe('Issue dropdown sidebar', () => { diff --git a/spec/frontend/line_highlighter_spec.js b/spec/frontend/line_highlighter_spec.js new file mode 100644 index 00000000000..0da1ea1df2d --- /dev/null +++ b/spec/frontend/line_highlighter_spec.js @@ -0,0 +1,268 @@ +/* eslint-disable no-return-assign, no-new, no-underscore-dangle */ + +import $ from 'jquery'; +import LineHighlighter from '~/line_highlighter'; + +describe('LineHighlighter', () => { + const testContext = {}; + + preloadFixtures('static/line_highlighter.html'); + const clickLine = (number, eventData = {}) => { + if ($.isEmptyObject(eventData)) { + return $(`#L${number}`).click(); + } + const e = $.Event('click', eventData); + return $(`#L${number}`).trigger(e); + }; + beforeEach(() => { + loadFixtures('static/line_highlighter.html'); + testContext.class = new LineHighlighter(); + testContext.css = testContext.class.highlightLineClass; + return (testContext.spies = { + __setLocationHash__: jest + .spyOn(testContext.class, '__setLocationHash__') + .mockImplementation(() => {}), + }); + }); + + describe('behavior', () => { + it('highlights one line given in the URL hash', () => { + new LineHighlighter({ hash: '#L13' }); + + expect($('#LC13')).toHaveClass(testContext.css); + }); + + it('highlights one line given in the URL hash with given CSS class name', () => { + 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', () => { + new LineHighlighter({ hash: '#L5-25' }); + + expect($(`.${testContext.css}`).length).toBe(21); + for (let line = 5; line <= 25; line += 1) { + expect($(`#LC${line}`)).toHaveClass(testContext.css); + } + }); + + it('scrolls to the first highlighted line on initial load', () => { + const spy = jest.spyOn($, 'scrollTo'); + new LineHighlighter({ hash: '#L5-25' }); + + expect(spy).toHaveBeenCalledWith('#L5', expect.anything()); + }); + + it('discards click events', () => { + const clickSpy = jest.fn(); + + $('a[data-line-number]').click(clickSpy); + + clickLine(13); + + expect(clickSpy.mock.calls[0][0].isDefaultPrevented()).toEqual(true); + }); + + it('handles garbage input from the hash', () => { + const func = () => { + return new LineHighlighter({ fileHolderSelector: '#blob-content-holder' }); + }; + + expect(func).not.toThrow(); + }); + + it('handles hashchange event', () => { + const highlighter = new LineHighlighter(); + + jest.spyOn(highlighter, 'highlightHash').mockImplementation(() => {}); + + window.dispatchEvent(new Event('hashchange'), 'L15'); + + expect(highlighter.highlightHash).toHaveBeenCalled(); + }); + }); + + describe('clickHandler', () => { + it('handles clicking on a child icon element', () => { + const spy = jest.spyOn(testContext.class, 'setHash'); + $('#L13 i') + .mousedown() + .click(); + + expect(spy).toHaveBeenCalledWith(13); + expect($('#LC13')).toHaveClass(testContext.css); + }); + + describe('without shiftKey', () => { + it('highlights one line when clicked', () => { + clickLine(13); + + expect($('#LC13')).toHaveClass(testContext.css); + }); + + it('unhighlights previously highlighted lines', () => { + clickLine(13); + clickLine(20); + + expect($('#LC13')).not.toHaveClass(testContext.css); + expect($('#LC20')).toHaveClass(testContext.css); + }); + + it('sets the hash', () => { + const spy = jest.spyOn(testContext.class, 'setHash'); + clickLine(13); + + expect(spy).toHaveBeenCalledWith(13); + }); + }); + + describe('with shiftKey', () => { + it('sets the hash', () => { + const spy = jest.spyOn(testContext.class, 'setHash'); + clickLine(13); + clickLine(20, { + shiftKey: true, + }); + + expect(spy).toHaveBeenCalledWith(13); + expect(spy).toHaveBeenCalledWith(13, 20); + }); + + describe('without existing highlight', () => { + it('highlights the clicked line', () => { + clickLine(13, { + shiftKey: true, + }); + + expect($('#LC13')).toHaveClass(testContext.css); + expect($(`.${testContext.css}`).length).toBe(1); + }); + + it('sets the hash', () => { + const spy = jest.spyOn(testContext.class, 'setHash'); + clickLine(13, { + shiftKey: true, + }); + + expect(spy).toHaveBeenCalledWith(13); + }); + }); + + describe('with existing single-line highlight', () => { + it('uses existing line as last line when target is lesser', () => { + clickLine(20); + clickLine(15, { + shiftKey: true, + }); + + expect($(`.${testContext.css}`).length).toBe(6); + for (let line = 15; line <= 20; line += 1) { + expect($(`#LC${line}`)).toHaveClass(testContext.css); + } + }); + + it('uses existing line as first line when target is greater', () => { + clickLine(5); + clickLine(10, { + shiftKey: true, + }); + + expect($(`.${testContext.css}`).length).toBe(6); + for (let line = 5; line <= 10; line += 1) { + expect($(`#LC${line}`)).toHaveClass(testContext.css); + } + }); + }); + + describe('with existing multi-line highlight', () => { + beforeEach(() => { + clickLine(10, { + shiftKey: true, + }); + clickLine(13, { + shiftKey: true, + }); + }); + + it('uses target as first line when it is less than existing first line', () => { + clickLine(5, { + shiftKey: true, + }); + + expect($(`.${testContext.css}`).length).toBe(6); + for (let line = 5; line <= 10; line += 1) { + expect($(`#LC${line}`)).toHaveClass(testContext.css); + } + }); + + it('uses target as last line when it is greater than existing first line', () => { + clickLine(15, { + shiftKey: true, + }); + + expect($(`.${testContext.css}`).length).toBe(6); + for (let line = 10; line <= 15; line += 1) { + expect($(`#LC${line}`)).toHaveClass(testContext.css); + } + }); + }); + }); + }); + + describe('hashToRange', () => { + beforeEach(() => { + testContext.subject = testContext.class.hashToRange; + }); + + it('extracts a single line number from the hash', () => { + expect(testContext.subject('#L5')).toEqual([5, null]); + }); + + it('extracts a range of line numbers from the hash', () => { + expect(testContext.subject('#L5-15')).toEqual([5, 15]); + }); + + it('returns [null, null] when the hash is not a line number', () => { + expect(testContext.subject('#foo')).toEqual([null, null]); + }); + }); + + describe('highlightLine', () => { + beforeEach(() => { + testContext.subject = testContext.class.highlightLine; + }); + + it('highlights the specified line', () => { + testContext.subject(13); + + expect($('#LC13')).toHaveClass(testContext.css); + }); + + it('accepts a String-based number', () => { + testContext.subject('13'); + + expect($('#LC13')).toHaveClass(testContext.css); + }); + }); + + describe('setHash', () => { + beforeEach(() => { + testContext.subject = testContext.class.setHash; + }); + + it('sets the location hash for a single line', () => { + testContext.subject(5); + + expect(testContext.spies.__setLocationHash__).toHaveBeenCalledWith('#L5'); + }); + + it('sets the location hash for a range', () => { + testContext.subject(5, 15); + + expect(testContext.spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15'); + }); + }); +}); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js index 2b5fde9532d..3d3be647d12 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/frontend/merge_request_tabs_spec.js @@ -5,12 +5,16 @@ import MergeRequestTabs from '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; import '~/lib/utils/common_utils'; import 'vendor/jquery.scrollTo'; -import initMrPage from './helpers/init_vue_mr_page_helper'; +import initMrPage from '../javascripts/helpers/init_vue_mr_page_helper'; -describe('MergeRequestTabs', function() { - let mrPageMock; +jest.mock('~/lib/utils/webpack', () => ({ + resetServiceWorkersPublicPath: jest.fn(), +})); + +describe('MergeRequestTabs', () => { + const testContext = {}; const stubLocation = {}; - const setLocation = function(stubs) { + const setLocation = stubs => { const defaults = { pathname: '', search: '', @@ -24,29 +28,25 @@ describe('MergeRequestTabs', function() { 'merge_requests/diff_comment.html', ); - beforeEach(function() { - mrPageMock = initMrPage(); - this.class = new MergeRequestTabs({ stubLocation }); + beforeEach(() => { + initMrPage(); + + testContext.class = new MergeRequestTabs({ stubLocation }); setLocation(); - this.spies = { - history: spyOn(window.history, 'pushState').and.callFake(function() {}), + testContext.spies = { + history: jest.spyOn(window.history, 'pushState').mockImplementation(() => {}), }; - }); - afterEach(function() { - this.class.unbindEvents(); - this.class.destroyPipelinesView(); - mrPageMock.restore(); - $('.js-merge-request-test').remove(); + gl.mrWidget = {}; }); - describe('opensInNewTab', function() { + describe('opensInNewTab', () => { const windowTarget = '_blank'; let clickTabParams; let tabUrl; - beforeEach(function() { + beforeEach(() => { loadFixtures('merge_requests/merge_request_with_task_list.html'); tabUrl = $('.commits-tab a').attr('href'); @@ -68,76 +68,76 @@ describe('MergeRequestTabs', function() { describe('meta click', () => { let metakeyEvent; - beforeEach(function() { + beforeEach(() => { metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); }); - it('opens page when commits link is clicked', function() { - spyOn(window, 'open').and.callFake(function(url, name) { + it('opens page when commits link is clicked', () => { + jest.spyOn(window, 'open').mockImplementation((url, name) => { expect(url).toEqual(tabUrl); expect(name).toEqual(windowTarget); }); - this.class.bindEvents(); + testContext.class.bindEvents(); $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent); expect(window.open).toHaveBeenCalled(); }); - it('opens page when commits badge is clicked', function() { - spyOn(window, 'open').and.callFake(function(url, name) { + it('opens page when commits badge is clicked', () => { + jest.spyOn(window, 'open').mockImplementation((url, name) => { expect(url).toEqual(tabUrl); expect(name).toEqual(windowTarget); }); - this.class.bindEvents(); + testContext.class.bindEvents(); $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent); expect(window.open).toHaveBeenCalled(); }); }); - it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function() { - spyOn(window, 'open').and.callFake(function(url, name) { + it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', () => { + jest.spyOn(window, 'open').mockImplementation((url, name) => { expect(url).toEqual(tabUrl); expect(name).toEqual(windowTarget); }); - this.class.clickTab({ ...clickTabParams, metaKey: true }); + testContext.class.clickTab({ ...clickTabParams, metaKey: true }); expect(window.open).toHaveBeenCalled(); }); - it('opens page tab in a new browser tab with Cmd+Click - Mac', function() { - spyOn(window, 'open').and.callFake(function(url, name) { + it('opens page tab in a new browser tab with Cmd+Click - Mac', () => { + jest.spyOn(window, 'open').mockImplementation((url, name) => { expect(url).toEqual(tabUrl); expect(name).toEqual(windowTarget); }); - this.class.clickTab({ ...clickTabParams, ctrlKey: true }); + testContext.class.clickTab({ ...clickTabParams, ctrlKey: true }); expect(window.open).toHaveBeenCalled(); }); - it('opens page tab in a new browser tab with Middle-click - Mac/PC', function() { - spyOn(window, 'open').and.callFake(function(url, name) { + it('opens page tab in a new browser tab with Middle-click - Mac/PC', () => { + jest.spyOn(window, 'open').mockImplementation((url, name) => { expect(url).toEqual(tabUrl); expect(name).toEqual(windowTarget); }); - this.class.clickTab({ ...clickTabParams, which: 2 }); + testContext.class.clickTab({ ...clickTabParams, which: 2 }); expect(window.open).toHaveBeenCalled(); }); }); - describe('setCurrentAction', function() { + describe('setCurrentAction', () => { let mock; - beforeEach(function() { + beforeEach(() => { mock = new MockAdapter(axios); mock.onAny().reply({ data: {} }); - this.subject = this.class.setCurrentAction; + testContext.subject = testContext.class.setCurrentAction; }); afterEach(() => { @@ -145,53 +145,53 @@ describe('MergeRequestTabs', function() { window.history.replaceState({}, '', '/'); }); - it('changes from commits', function() { + it('changes from commits', () => { setLocation({ pathname: '/foo/bar/-/merge_requests/1/commits', }); - expect(this.subject('show')).toBe('/foo/bar/-/merge_requests/1'); - expect(this.subject('diffs')).toBe('/foo/bar/-/merge_requests/1/diffs'); + expect(testContext.subject('show')).toBe('/foo/bar/-/merge_requests/1'); + expect(testContext.subject('diffs')).toBe('/foo/bar/-/merge_requests/1/diffs'); }); - it('changes from diffs', function() { + it('changes from diffs', () => { setLocation({ pathname: '/foo/bar/-/merge_requests/1/diffs', }); - expect(this.subject('show')).toBe('/foo/bar/-/merge_requests/1'); - expect(this.subject('commits')).toBe('/foo/bar/-/merge_requests/1/commits'); + expect(testContext.subject('show')).toBe('/foo/bar/-/merge_requests/1'); + expect(testContext.subject('commits')).toBe('/foo/bar/-/merge_requests/1/commits'); }); - it('changes from diffs.html', function() { + it('changes from diffs.html', () => { setLocation({ pathname: '/foo/bar/-/merge_requests/1/diffs.html', }); - expect(this.subject('show')).toBe('/foo/bar/-/merge_requests/1'); - expect(this.subject('commits')).toBe('/foo/bar/-/merge_requests/1/commits'); + expect(testContext.subject('show')).toBe('/foo/bar/-/merge_requests/1'); + expect(testContext.subject('commits')).toBe('/foo/bar/-/merge_requests/1/commits'); }); - it('changes from notes', function() { + it('changes from notes', () => { setLocation({ pathname: '/foo/bar/-/merge_requests/1', }); - expect(this.subject('diffs')).toBe('/foo/bar/-/merge_requests/1/diffs'); - expect(this.subject('commits')).toBe('/foo/bar/-/merge_requests/1/commits'); + expect(testContext.subject('diffs')).toBe('/foo/bar/-/merge_requests/1/diffs'); + expect(testContext.subject('commits')).toBe('/foo/bar/-/merge_requests/1/commits'); }); - it('includes search parameters and hash string', function() { + it('includes search parameters and hash string', () => { setLocation({ pathname: '/foo/bar/-/merge_requests/1/diffs', search: '?view=parallel', hash: '#L15-35', }); - expect(this.subject('show')).toBe('/foo/bar/-/merge_requests/1?view=parallel#L15-35'); + expect(testContext.subject('show')).toBe('/foo/bar/-/merge_requests/1?view=parallel#L15-35'); }); - it('replaces the current history state', function() { + it('replaces the current history state', () => { setLocation({ pathname: '/foo/bar/-/merge_requests/1', }); @@ -204,9 +204,9 @@ describe('MergeRequestTabs', function() { window.location.href, ); - const newState = this.subject('commits'); + const newState = testContext.subject('commits'); - expect(this.spies.history).toHaveBeenCalledWith( + expect(testContext.spies.history).toHaveBeenCalledWith( { url: newState, action: 'commits', @@ -216,16 +216,16 @@ describe('MergeRequestTabs', function() { ); }); - it('treats "show" like "notes"', function() { + it('treats "show" like "notes"', () => { setLocation({ pathname: '/foo/bar/-/merge_requests/1/commits', }); - expect(this.subject('show')).toBe('/foo/bar/-/merge_requests/1'); + expect(testContext.subject('show')).toBe('/foo/bar/-/merge_requests/1'); }); }); - describe('expandViewContainer', function() { + describe('expandViewContainer', () => { beforeEach(() => { $('body').append( '<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>', @@ -236,59 +236,58 @@ describe('MergeRequestTabs', function() { $('.content-wrapper').remove(); }); - it('removes container-limited from containers', function() { - this.class.expandViewContainer(); + it('removes container-limited from containers', () => { + testContext.class.expandViewContainer(); - expect($('.content-wrapper')).not.toContainElement('.container-limited'); + expect($('.content-wrapper .container-limited')).toHaveLength(0); }); - it('does not add container-limited when fluid layout is prefered', function() { + it('does not add container-limited when fluid layout is prefered', () => { $('.content-wrapper .container-fluid').removeClass('container-limited'); - this.class.expandViewContainer(false); + testContext.class.expandViewContainer(false); - expect($('.content-wrapper')).not.toContainElement('.container-limited'); + expect($('.content-wrapper .container-limited')).toHaveLength(0); }); - it('does remove container-limited from breadcrumbs', function() { + it('does remove container-limited from breadcrumbs', () => { $('.container-limited').addClass('breadcrumbs'); - this.class.expandViewContainer(); + testContext.class.expandViewContainer(); - expect($('.content-wrapper')).toContainElement('.container-limited'); + expect($('.content-wrapper .container-limited')).toHaveLength(1); }); }); - describe('tabShown', function() { + describe('tabShown', () => { const mainContent = document.createElement('div'); const tabContent = document.createElement('div'); - beforeEach(function() { - spyOn(mainContent, 'getBoundingClientRect').and.returnValue({ top: 10 }); - spyOn(tabContent, 'getBoundingClientRect').and.returnValue({ top: 100 }); - spyOn(document, 'querySelector').and.callFake(function(selector) { + beforeEach(() => { + jest.spyOn(mainContent, 'getBoundingClientRect').mockReturnValue({ top: 10 }); + jest.spyOn(tabContent, 'getBoundingClientRect').mockReturnValue({ top: 100 }); + jest.spyOn(document, 'querySelector').mockImplementation(selector => { return selector === '.content-wrapper' ? mainContent : tabContent; }); - this.class.currentAction = 'commits'; + testContext.class.currentAction = 'commits'; }); - it('calls window scrollTo with options if document has scrollBehavior', function() { + it('calls window scrollTo with options if document has scrollBehavior', () => { document.documentElement.style.scrollBehavior = ''; - spyOn(window, 'scrollTo'); + jest.spyOn(window, 'scrollTo').mockImplementation(() => {}); - this.class.tabShown('commits', 'foobar'); + testContext.class.tabShown('commits', 'foobar'); - expect(window.scrollTo.calls.first().args[0]).toEqual({ top: 39, behavior: 'smooth' }); + expect(window.scrollTo.mock.calls[0][0]).toEqual({ top: 39, behavior: 'smooth' }); }); - it('calls window scrollTo with two args if document does not have scrollBehavior', function() { - spyOnProperty(document.documentElement, 'style', 'get').and.returnValue({}); - - spyOn(window, 'scrollTo'); + it('calls window scrollTo with two args if document does not have scrollBehavior', () => { + jest.spyOn(document.documentElement, 'style', 'get').mockReturnValue({}); + jest.spyOn(window, 'scrollTo').mockImplementation(() => {}); - this.class.tabShown('commits', 'foobar'); + testContext.class.tabShown('commits', 'foobar'); - expect(window.scrollTo.calls.first().args).toEqual([0, 39]); + expect(window.scrollTo.mock.calls[0]).toEqual([0, 39]); }); }); }); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/frontend/right_sidebar_spec.js index 9565e3ce546..d80d80152a5 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/frontend/right_sidebar_spec.js @@ -10,7 +10,7 @@ let $icon = null; let $page = null; let $labelsIcon = null; -const assertSidebarState = function(state) { +const assertSidebarState = state => { const shouldBeExpanded = state === 'expanded'; const shouldBeCollapsed = state === 'collapsed'; expect($aside.hasClass('right-sidebar-expanded')).toBe(shouldBeExpanded); @@ -21,14 +21,13 @@ const assertSidebarState = function(state) { expect($icon.hasClass('fa-angle-double-left')).toBe(shouldBeCollapsed); }; -describe('RightSidebar', function() { +describe('RightSidebar', () => { describe('fixture tests', () => { const fixtureName = 'issues/open-issue.html'; preloadFixtures(fixtureName); - loadJSONFixtures('todos/todos.json'); let mock; - beforeEach(function() { + beforeEach(() => { loadFixtures(fixtureName); mock = new MockAdapter(axios); new Sidebar(); // eslint-disable-line no-new @@ -43,7 +42,7 @@ describe('RightSidebar', function() { mock.restore(); }); - it('should expand/collapse the sidebar when arrow is clicked', function() { + it('should expand/collapse the sidebar when arrow is clicked', () => { assertSidebarState('expanded'); $toggle.click(); assertSidebarState('collapsed'); @@ -51,28 +50,29 @@ describe('RightSidebar', function() { assertSidebarState('expanded'); }); - it('should float over the page and when sidebar icons clicked', function() { + it('should float over the page and when sidebar icons clicked', () => { $labelsIcon.click(); assertSidebarState('expanded'); }); - it('should collapse when the icon arrow clicked while it is floating on page', function() { + it('should collapse when the icon arrow clicked while it is floating on page', () => { $labelsIcon.click(); assertSidebarState('expanded'); $toggle.click(); assertSidebarState('collapsed'); }); - it('should broadcast todo:toggle event when add todo clicked', function(done) { + it('should broadcast todo:toggle event when add todo clicked', done => { const todos = getJSONFixture('todos/todos.json'); mock.onPost(/(.*)\/todos$/).reply(200, todos); - const todoToggleSpy = spyOnEvent(document, 'todo:toggle'); + const todoToggleSpy = jest.fn(); + $(document).on('todo:toggle', todoToggleSpy); $('.issuable-sidebar-header .js-issuable-todo').click(); - setTimeout(() => { - expect(todoToggleSpy.calls.count()).toEqual(1); + setImmediate(() => { + expect(todoToggleSpy.mock.calls.length).toEqual(1); done(); }); diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js new file mode 100644 index 00000000000..3d16074154c --- /dev/null +++ b/spec/frontend/shortcuts_spec.js @@ -0,0 +1,46 @@ +import $ from 'jquery'; +import Shortcuts from '~/behaviors/shortcuts/shortcuts'; + +describe('Shortcuts', () => { + const fixtureName = 'snippets/show.html'; + const createEvent = (type, target) => + $.Event(type, { + target, + }); + + preloadFixtures(fixtureName); + + describe('toggleMarkdownPreview', () => { + beforeEach(() => { + loadFixtures(fixtureName); + + jest.spyOn(document.querySelector('.js-new-note-form .js-md-preview-button'), 'focus'); + jest.spyOn(document.querySelector('.edit-note .js-md-preview-button'), 'focus'); + + new Shortcuts(); // eslint-disable-line no-new + }); + + it('focuses preview button in form', () => { + Shortcuts.toggleMarkdownPreview( + createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text')), + ); + + expect( + document.querySelector('.js-new-note-form .js-md-preview-button').focus, + ).toHaveBeenCalled(); + }); + + it('focues preview button inside edit comment form', () => { + document.querySelector('.js-note-edit').click(); + + Shortcuts.toggleMarkdownPreview( + createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text')), + ); + + expect( + document.querySelector('.js-new-note-form .js-md-preview-button').focus, + ).not.toHaveBeenCalled(); + expect(document.querySelector('.edit-note .js-md-preview-button').focus).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js index 6ac22fca2d3..0367b9cc924 100644 --- a/spec/javascripts/user_popovers_spec.js +++ b/spec/frontend/user_popovers_spec.js @@ -26,10 +26,12 @@ describe('User Popovers', () => { loadFixtures(fixtureTemplate); const usersCacheSpy = () => Promise.resolve(dummyUser); - spyOn(UsersCache, 'retrieveById').and.callFake(userId => usersCacheSpy(userId)); + jest.spyOn(UsersCache, 'retrieveById').mockImplementation(userId => usersCacheSpy(userId)); const userStatusCacheSpy = () => Promise.resolve(dummyUserStatus); - spyOn(UsersCache, 'retrieveStatusById').and.callFake(userId => userStatusCacheSpy(userId)); + jest + .spyOn(UsersCache, 'retrieveStatusById') + .mockImplementation(userId => userStatusCacheSpy(userId)); popovers = initUserPopovers(document.querySelectorAll(selector)); }); @@ -53,6 +55,8 @@ describe('User Popovers', () => { let userLink; beforeEach(() => { + UsersCache.retrieveById.mockReset(); + userLink = document.querySelector(selector); triggerEvent('mouseenter', userLink); @@ -68,7 +72,7 @@ describe('User Popovers', () => { const [firstPopover] = popovers; expect(firstPopover.$props.user).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ name, userId, username, diff --git a/spec/javascripts/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js index 5dee11b3810..8e0d170289b 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/frontend/zen_mode_spec.js @@ -1,10 +1,13 @@ import $ from 'jquery'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; import Dropzone from 'dropzone'; import Mousetrap from 'mousetrap'; import ZenMode from '~/zen_mode'; import initNotes from '~/init_notes'; describe('ZenMode', () => { + let mock; let zen; let dropzoneForElementSpy; const fixtureName = 'snippets/show.html'; @@ -28,10 +31,13 @@ describe('ZenMode', () => { } beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet().reply(200); + loadFixtures(fixtureName); initNotes(); - dropzoneForElementSpy = spyOn(Dropzone, 'forElement').and.callFake(() => ({ + dropzoneForElementSpy = jest.spyOn(Dropzone, 'forElement').mockImplementation(() => ({ enable: () => true, })); zen = new ZenMode(); @@ -49,20 +55,20 @@ describe('ZenMode', () => { $('.div-dropzone').addClass('js-invalid-dropzone'); exitZen(); - expect(dropzoneForElementSpy.calls.count()).toEqual(0); + expect(dropzoneForElementSpy.mock.calls.length).toEqual(0); }); it('should call dropzone if element is dropzone valid', () => { $('.div-dropzone').removeClass('js-invalid-dropzone'); exitZen(); - expect(dropzoneForElementSpy.calls.count()).toEqual(2); + expect(dropzoneForElementSpy.mock.calls.length).toEqual(2); }); }); describe('on enter', () => { it('pauses Mousetrap', () => { - const mouseTrapPauseSpy = spyOn(Mousetrap, 'pause'); + const mouseTrapPauseSpy = jest.spyOn(Mousetrap, 'pause'); enterZen(); expect(mouseTrapPauseSpy).toHaveBeenCalled(); @@ -90,14 +96,14 @@ describe('ZenMode', () => { beforeEach(enterZen); it('unpauses Mousetrap', () => { - const mouseTrapUnpauseSpy = spyOn(Mousetrap, 'unpause'); + const mouseTrapUnpauseSpy = jest.spyOn(Mousetrap, 'unpause'); exitZen(); expect(mouseTrapUnpauseSpy).toHaveBeenCalled(); }); it('restores the scroll position', () => { - spyOn(zen, 'scrollTo'); + jest.spyOn(zen, 'scrollTo').mockImplementation(() => {}); exitZen(); expect(zen.scrollTo).toHaveBeenCalled(); diff --git a/spec/javascripts/comment_type_toggle_spec.js b/spec/javascripts/comment_type_toggle_spec.js deleted file mode 100644 index 8b1217a000f..00000000000 --- a/spec/javascripts/comment_type_toggle_spec.js +++ /dev/null @@ -1,168 +0,0 @@ -import CommentTypeToggle from '~/comment_type_toggle'; -import InputSetter from '~/droplab/plugins/input_setter'; - -describe('CommentTypeToggle', function() { - describe('class constructor', function() { - beforeEach(function() { - this.dropdownTrigger = {}; - this.dropdownList = {}; - this.noteTypeInput = {}; - this.submitButton = {}; - this.closeButton = {}; - - this.commentTypeToggle = new CommentTypeToggle({ - dropdownTrigger: this.dropdownTrigger, - dropdownList: this.dropdownList, - noteTypeInput: this.noteTypeInput, - submitButton: this.submitButton, - closeButton: this.closeButton, - }); - }); - - it('should set .dropdownTrigger', function() { - expect(this.commentTypeToggle.dropdownTrigger).toBe(this.dropdownTrigger); - }); - - it('should set .dropdownList', function() { - expect(this.commentTypeToggle.dropdownList).toBe(this.dropdownList); - }); - - it('should set .noteTypeInput', function() { - expect(this.commentTypeToggle.noteTypeInput).toBe(this.noteTypeInput); - }); - - it('should set .submitButton', function() { - expect(this.commentTypeToggle.submitButton).toBe(this.submitButton); - }); - - it('should set .closeButton', function() { - expect(this.commentTypeToggle.closeButton).toBe(this.closeButton); - }); - - it('should set .reopenButton', function() { - expect(this.commentTypeToggle.reopenButton).toBe(this.reopenButton); - }); - }); - - describe('initDroplab', function() { - beforeEach(function() { - this.commentTypeToggle = { - dropdownTrigger: {}, - dropdownList: {}, - noteTypeInput: {}, - submitButton: {}, - closeButton: {}, - setConfig: () => {}, - }; - this.config = {}; - - this.droplab = jasmine.createSpyObj('droplab', ['init']); - - this.droplabConstructor = spyOnDependency(CommentTypeToggle, 'DropLab').and.returnValue( - this.droplab, - ); - spyOn(this.commentTypeToggle, 'setConfig').and.returnValue(this.config); - - CommentTypeToggle.prototype.initDroplab.call(this.commentTypeToggle); - }); - - it('should instantiate a DropLab instance', function() { - expect(this.droplabConstructor).toHaveBeenCalled(); - }); - - it('should set .droplab', function() { - expect(this.commentTypeToggle.droplab).toBe(this.droplab); - }); - - it('should call .setConfig', function() { - expect(this.commentTypeToggle.setConfig).toHaveBeenCalled(); - }); - - it('should call DropLab.prototype.init', function() { - expect(this.droplab.init).toHaveBeenCalledWith( - this.commentTypeToggle.dropdownTrigger, - this.commentTypeToggle.dropdownList, - [InputSetter], - this.config, - ); - }); - }); - - describe('setConfig', function() { - describe('if no .closeButton is provided', function() { - beforeEach(function() { - this.commentTypeToggle = { - dropdownTrigger: {}, - dropdownList: {}, - noteTypeInput: {}, - submitButton: {}, - reopenButton: {}, - }; - - this.setConfig = CommentTypeToggle.prototype.setConfig.call(this.commentTypeToggle); - }); - - it('should not add .closeButton related InputSetter config', function() { - expect(this.setConfig).toEqual({ - InputSetter: [ - { - input: this.commentTypeToggle.noteTypeInput, - valueAttribute: 'data-value', - }, - { - input: this.commentTypeToggle.submitButton, - valueAttribute: 'data-submit-text', - }, - { - input: this.commentTypeToggle.reopenButton, - valueAttribute: 'data-reopen-text', - }, - { - input: this.commentTypeToggle.reopenButton, - valueAttribute: 'data-reopen-text', - inputAttribute: 'data-alternative-text', - }, - ], - }); - }); - }); - - describe('if no .reopenButton is provided', function() { - beforeEach(function() { - this.commentTypeToggle = { - dropdownTrigger: {}, - dropdownList: {}, - noteTypeInput: {}, - submitButton: {}, - closeButton: {}, - }; - - this.setConfig = CommentTypeToggle.prototype.setConfig.call(this.commentTypeToggle); - }); - - it('should not add .reopenButton related InputSetter config', function() { - expect(this.setConfig).toEqual({ - InputSetter: [ - { - input: this.commentTypeToggle.noteTypeInput, - valueAttribute: 'data-value', - }, - { - input: this.commentTypeToggle.submitButton, - valueAttribute: 'data-submit-text', - }, - { - input: this.commentTypeToggle.closeButton, - valueAttribute: 'data-close-text', - }, - { - input: this.commentTypeToggle.closeButton, - valueAttribute: 'data-close-text', - inputAttribute: 'data-alternative-text', - }, - ], - }); - }); - }); - }); -}); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js deleted file mode 100644 index bedab0fd003..00000000000 --- a/spec/javascripts/line_highlighter_spec.js +++ /dev/null @@ -1,261 +0,0 @@ -/* eslint-disable dot-notation, no-return-assign, no-new, no-underscore-dangle */ - -import $ from 'jquery'; -import LineHighlighter from '~/line_highlighter'; - -describe('LineHighlighter', function() { - preloadFixtures('static/line_highlighter.html'); - const clickLine = function(number, eventData = {}) { - if ($.isEmptyObject(eventData)) { - return $(`#L${number}`).click(); - } - const e = $.Event('click', eventData); - return $(`#L${number}`).trigger(e); - }; - beforeEach(function() { - loadFixtures('static/line_highlighter.html'); - this['class'] = new LineHighlighter(); - 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({ hash: '#L13' }); - - 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() { - new LineHighlighter({ hash: '#L5-25' }); - - expect($(`.${this.css}`).length).toBe(21); - for (let line = 5; line <= 25; line += 1) { - expect($(`#LC${line}`)).toHaveClass(this.css); - } - }); - - it('scrolls to the first highlighted line on initial load', function() { - const spy = spyOn($, 'scrollTo'); - new LineHighlighter({ hash: '#L5-25' }); - - expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()); - }); - - it('discards click events', function() { - const spy = spyOnEvent('a[data-line-number]', 'click'); - clickLine(13); - - expect(spy).toHaveBeenPrevented(); - }); - - it('handles garbage input from the hash', function() { - const func = function() { - return new LineHighlighter({ fileHolderSelector: '#blob-content-holder' }); - }; - - expect(func).not.toThrow(); - }); - - it('handles hashchange event', () => { - const highlighter = new LineHighlighter(); - - spyOn(highlighter, 'highlightHash'); - - window.dispatchEvent(new Event('hashchange'), 'L15'); - - expect(highlighter.highlightHash).toHaveBeenCalled(); - }); - }); - - describe('clickHandler', function() { - it('handles clicking on a child icon element', function() { - const spy = spyOn(this['class'], 'setHash').and.callThrough(); - $('#L13 i') - .mousedown() - .click(); - - expect(spy).toHaveBeenCalledWith(13); - expect($('#LC13')).toHaveClass(this.css); - }); - - describe('without shiftKey', function() { - it('highlights one line when clicked', function() { - clickLine(13); - - expect($('#LC13')).toHaveClass(this.css); - }); - - it('unhighlights previously highlighted lines', function() { - clickLine(13); - clickLine(20); - - expect($('#LC13')).not.toHaveClass(this.css); - expect($('#LC20')).toHaveClass(this.css); - }); - - it('sets the hash', function() { - const spy = spyOn(this['class'], 'setHash').and.callThrough(); - clickLine(13); - - expect(spy).toHaveBeenCalledWith(13); - }); - }); - - describe('with shiftKey', function() { - it('sets the hash', function() { - const spy = spyOn(this['class'], 'setHash').and.callThrough(); - clickLine(13); - clickLine(20, { - shiftKey: true, - }); - - expect(spy).toHaveBeenCalledWith(13); - expect(spy).toHaveBeenCalledWith(13, 20); - }); - - describe('without existing highlight', function() { - it('highlights the clicked line', function() { - clickLine(13, { - shiftKey: true, - }); - - expect($('#LC13')).toHaveClass(this.css); - expect($(`.${this.css}`).length).toBe(1); - }); - - it('sets the hash', function() { - const spy = spyOn(this['class'], 'setHash'); - clickLine(13, { - shiftKey: true, - }); - - expect(spy).toHaveBeenCalledWith(13); - }); - }); - - describe('with existing single-line highlight', function() { - it('uses existing line as last line when target is lesser', function() { - clickLine(20); - clickLine(15, { - shiftKey: true, - }); - - expect($(`.${this.css}`).length).toBe(6); - for (let line = 15; line <= 20; line += 1) { - expect($(`#LC${line}`)).toHaveClass(this.css); - } - }); - - it('uses existing line as first line when target is greater', function() { - clickLine(5); - clickLine(10, { - shiftKey: true, - }); - - expect($(`.${this.css}`).length).toBe(6); - for (let line = 5; line <= 10; line += 1) { - expect($(`#LC${line}`)).toHaveClass(this.css); - } - }); - }); - - describe('with existing multi-line highlight', function() { - beforeEach(function() { - clickLine(10, { - shiftKey: true, - }); - clickLine(13, { - shiftKey: true, - }); - }); - - it('uses target as first line when it is less than existing first line', function() { - clickLine(5, { - shiftKey: true, - }); - - expect($(`.${this.css}`).length).toBe(6); - for (let line = 5; line <= 10; line += 1) { - expect($(`#LC${line}`)).toHaveClass(this.css); - } - }); - - it('uses target as last line when it is greater than existing first line', function() { - clickLine(15, { - shiftKey: true, - }); - - expect($(`.${this.css}`).length).toBe(6); - for (let line = 10; line <= 15; line += 1) { - expect($(`#LC${line}`)).toHaveClass(this.css); - } - }); - }); - }); - }); - - describe('hashToRange', function() { - beforeEach(function() { - this.subject = this['class'].hashToRange; - }); - - it('extracts a single line number from the hash', function() { - expect(this.subject('#L5')).toEqual([5, null]); - }); - - it('extracts a range of line numbers from the hash', function() { - expect(this.subject('#L5-15')).toEqual([5, 15]); - }); - - it('returns [null, null] when the hash is not a line number', function() { - expect(this.subject('#foo')).toEqual([null, null]); - }); - }); - - describe('highlightLine', function() { - beforeEach(function() { - this.subject = this['class'].highlightLine; - }); - - it('highlights the specified line', function() { - this.subject(13); - - expect($('#LC13')).toHaveClass(this.css); - }); - - it('accepts a String-based number', function() { - this.subject('13'); - - expect($('#LC13')).toHaveClass(this.css); - }); - }); - - describe('setHash', function() { - beforeEach(function() { - this.subject = this['class'].setHash; - }); - - it('sets the location hash for a single line', function() { - this.subject(5); - - expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5'); - }); - - it('sets the location hash for a range', function() { - this.subject(5, 15); - - expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15'); - }); - }); -}); diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js deleted file mode 100644 index df7012bb659..00000000000 --- a/spec/javascripts/shortcuts_spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import $ from 'jquery'; -import Shortcuts from '~/behaviors/shortcuts/shortcuts'; - -describe('Shortcuts', () => { - const fixtureName = 'snippets/show.html'; - const createEvent = (type, target) => - $.Event(type, { - target, - }); - - preloadFixtures(fixtureName); - - describe('toggleMarkdownPreview', () => { - beforeEach(() => { - loadFixtures(fixtureName); - - spyOnEvent('.js-new-note-form .js-md-preview-button', 'focus'); - spyOnEvent('.edit-note .js-md-preview-button', 'focus'); - - new Shortcuts(); // eslint-disable-line no-new - }); - - it('focuses preview button in form', () => { - Shortcuts.toggleMarkdownPreview( - createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text')), - ); - - expect('focus').toHaveBeenTriggeredOn('.js-new-note-form .js-md-preview-button'); - }); - - it('focues preview button inside edit comment form', done => { - document.querySelector('.js-note-edit').click(); - - setTimeout(() => { - Shortcuts.toggleMarkdownPreview( - createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text')), - ); - - expect('focus').not.toHaveBeenTriggeredOn('.js-new-note-form .js-md-preview-button'); - expect('focus').toHaveBeenTriggeredOn('.edit-note .js-md-preview-button'); - - done(); - }); - }); - }); -}); diff --git a/spec/lib/gitlab/routing_spec.rb b/spec/lib/gitlab/routing_spec.rb index 965564cb83b..5446d6559fe 100644 --- a/spec/lib/gitlab/routing_spec.rb +++ b/spec/lib/gitlab/routing_spec.rb @@ -22,4 +22,25 @@ describe Gitlab::Routing do expect(subject).to respond_to(:namespace_project_path) end end + + describe Gitlab::Routing::LegacyRedirector do + subject { described_class.new(:wikis) } + + let(:request) { double(:request, path: path, query_string: '') } + let(:path) { '/gitlab-org/gitlab-test/wikis/home' } + + it 'returns "-" scoped url' do + expect(subject.call({}, request)).to eq('/gitlab-org/gitlab-test/-/wikis/home') + end + + context 'invalid uri characters' do + let(:path) { '/gitlab-org/gitlab-test/wikis/home[' } + + it 'raises error' do + expect do + subject.call({}, request) + end.to raise_error(ActionController::RoutingError) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5f4a3dbd99c..d7c4695203d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -4812,6 +4812,32 @@ describe Project do end end + describe '#execute_services' do + let(:service) { create(:slack_service, push_events: true, merge_requests_events: false, active: true) } + + it 'executes services with the specified scope' do + data = 'any data' + + expect(SlackService).to receive(:allocate).and_wrap_original do |method| + method.call.tap do |instance| + expect(instance).to receive(:async_execute).with(data).once + end + end + + service.project.execute_services(data, :push_hooks) + end + + it 'does not execute services that don\'t match the specified scope' do + expect(SlackService).not_to receive(:allocate).and_wrap_original do |method| + method.call.tap do |instance| + expect(instance).not_to receive(:async_execute) + end + end + + service.project.execute_services(anything, :merge_request_hooks) + end + end + describe '#has_active_hooks?' do let_it_be(:project) { create(:project) } |