diff options
author | Felipe Artur <fcardozo@gitlab.com> | 2018-06-21 12:22:40 +0000 |
---|---|---|
committer | Tim Zallmann <tzallmann@gitlab.com> | 2018-06-21 12:22:40 +0000 |
commit | 3e66795ef1ff1228906239763910b051d8afcc37 (patch) | |
tree | df6424d9ec008f5d1da455f8465681b371c4a11e /spec/javascripts | |
parent | 14e35ac9b19d358d84e0cfd167f74036937285b6 (diff) | |
download | gitlab-ce-3e66795ef1ff1228906239763910b051d8afcc37.tar.gz |
Changes tab VUE refactoring
Diffstat (limited to 'spec/javascripts')
59 files changed, 3681 insertions, 1051 deletions
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index ff6020c8fdd..ada26b37f4a 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, no-unused-vars, quotes, prefer-template, max-len */ +/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-expressions, no-unused-vars, prefer-template, max-len */ import $ from 'jquery'; import Cookies from 'js-cookie'; @@ -21,20 +21,21 @@ import '~/lib/utils/common_utils'; return setTimeout(function() { assertFn(); return done(); - // Maybe jasmine.clock here? + // Maybe jasmine.clock here? }, 333); }; describe('AwardsHandler', function() { - preloadFixtures('merge_requests/diff_comment.html.raw'); + preloadFixtures('snippets/show.html.raw'); beforeEach(function(done) { - loadFixtures('merge_requests/diff_comment.html.raw'); - $('body').attr('data-page', 'projects:merge_requests:show'); - loadAwardsHandler(true).then((obj) => { - awardsHandler = obj; - spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); - done(); - }).catch(fail); + loadFixtures('snippets/show.html.raw'); + loadAwardsHandler(true) + .then(obj => { + awardsHandler = obj; + spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); + done(); + }) + .catch(fail); let isEmojiMenuBuilt = false; openAndWaitForEmojiMenu = function() { @@ -42,7 +43,9 @@ import '~/lib/utils/common_utils'; if (isEmojiMenuBuilt) { resolve(); } else { - $('.js-add-award').eq(0).click(); + $('.js-add-award') + .eq(0) + .click(); const $menu = $('.emoji-menu'); $menu.one('build-emoji-menu-finish', () => { isEmojiMenuBuilt = true; @@ -63,7 +66,9 @@ import '~/lib/utils/common_utils'; }); describe('::showEmojiMenu', function() { it('should show emoji menu when Add emoji button clicked', function(done) { - $('.js-add-award').eq(0).click(); + $('.js-add-award') + .eq(0) + .click(); return lazyAssert(done, function() { var $emojiMenu; $emojiMenu = $('.emoji-menu'); @@ -81,7 +86,9 @@ import '~/lib/utils/common_utils'; }); }); it('should remove emoji menu when body is clicked', function(done) { - $('.js-add-award').eq(0).click(); + $('.js-add-award') + .eq(0) + .click(); return lazyAssert(done, function() { var $emojiMenu; $emojiMenu = $('.emoji-menu'); @@ -92,7 +99,9 @@ import '~/lib/utils/common_utils'; }); }); it('should not remove emoji menu when search is clicked', function(done) { - $('.js-add-award').eq(0).click(); + $('.js-add-award') + .eq(0) + .click(); return lazyAssert(done, function() { var $emojiMenu; $emojiMenu = $('.emoji-menu'); @@ -103,6 +112,7 @@ import '~/lib/utils/common_utils'; }); }); }); + describe('::addAwardToEmojiBar', function() { it('should add emoji to votes block', function() { var $emojiButton, $votesBlock; @@ -139,7 +149,9 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); $thumbsUpEmoji.attr('data-title', 'sam'); awardsHandler.userAuthored($thumbsUpEmoji); - return expect($thumbsUpEmoji.data("originalTitle")).toBe("You cannot vote on your own issue, MR and note"); + return expect($thumbsUpEmoji.data('originalTitle')).toBe( + 'You cannot vote on your own issue, MR and note', + ); }); it('should restore tooltip back to initial vote list', function() { var $thumbsUpEmoji, $votesBlock; @@ -150,12 +162,14 @@ import '~/lib/utils/common_utils'; awardsHandler.userAuthored($thumbsUpEmoji); jasmine.clock().tick(2801); jasmine.clock().uninstall(); - return expect($thumbsUpEmoji.data("originalTitle")).toBe("sam"); + return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam'); }); }); describe('::getAwardUrl', function() { return it('returns the url for request', function() { - return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1/toggle_award_emoji'); + return expect(awardsHandler.getAwardUrl()).toBe( + 'http://test.host/snippets/1/toggle_award_emoji', + ); }); }); describe('::addAward and ::checkMutuality', function() { @@ -195,7 +209,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("originalTitle")).toBe('You, sam, jerry, max, and andy'); + return expect($thumbsUpEmoji.data('originalTitle')).toBe('You, sam, jerry, max, and andy'); }); return it('handles the special case where "You" is not cleanly comma seperated', function() { var $thumbsUpEmoji, $votesBlock, awardUrl; @@ -205,7 +219,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.attr('data-title', 'sam'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("originalTitle")).toBe('You and sam'); + return expect($thumbsUpEmoji.data('originalTitle')).toBe('You and sam'); }); }); describe('::removeYouToUserList', function() { @@ -218,7 +232,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.addClass('active'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam, jerry, max, and andy'); + return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam, jerry, max, and andy'); }); return it('handles the special case where "You" is not cleanly comma seperated', function() { var $thumbsUpEmoji, $votesBlock, awardUrl; @@ -229,7 +243,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.addClass('active'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam'); + return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam'); }); }); describe('::searchEmojis', () => { @@ -245,7 +259,7 @@ import '~/lib/utils/common_utils'; expect($('.js-emoji-menu-search').val()).toBe('ali'); }) .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); @@ -263,7 +277,7 @@ import '~/lib/utils/common_utils'; expect($('.js-emoji-menu-search').val()).toBe(''); }) .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); @@ -272,37 +286,40 @@ import '~/lib/utils/common_utils'; describe('emoji menu', function() { const emojiSelector = '[data-name="sunglasses"]'; const openEmojiMenuAndAddEmoji = function() { - return openAndWaitForEmojiMenu() - .then(() => { - const $menu = $('.emoji-menu'); - const $block = $('.js-awards-block'); - const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector); + return openAndWaitForEmojiMenu().then(() => { + const $menu = $('.emoji-menu'); + const $block = $('.js-awards-block'); + const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector); - expect($emoji.length).toBe(1); - expect($block.find(emojiSelector).length).toBe(0); - $emoji.click(); - expect($menu.hasClass('.is-visible')).toBe(false); - expect($block.find(emojiSelector).length).toBe(1); - }); + expect($emoji.length).toBe(1); + expect($block.find(emojiSelector).length).toBe(0); + $emoji.click(); + expect($menu.hasClass('.is-visible')).toBe(false); + expect($block.find(emojiSelector).length).toBe(1); + }); }; it('should add selected emoji to awards block', function(done) { return openEmojiMenuAndAddEmoji() .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); it('should remove already selected emoji', function(done) { return openEmojiMenuAndAddEmoji() .then(() => { - $('.js-add-award').eq(0).click(); + $('.js-add-award') + .eq(0) + .click(); const $block = $('.js-awards-block'); - const $emoji = $('.emoji-menu').find(`.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`); + const $emoji = $('.emoji-menu').find( + `.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`, + ); $emoji.click(); expect($block.find(emojiSelector).length).toBe(0); }) .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); @@ -318,12 +335,12 @@ import '~/lib/utils/common_utils'; return openAndWaitForEmojiMenu() .then(() => { const emojiMenu = document.querySelector('.emoji-menu'); - Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), (title) => { + Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title => { expect(title.textContent.trim().toLowerCase()).not.toBe('frequently used'); }); }) .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); @@ -334,14 +351,15 @@ import '~/lib/utils/common_utils'; return openAndWaitForEmojiMenu() .then(() => { const emojiMenu = document.querySelector('.emoji-menu'); - const hasFrequentlyUsedHeading = Array.prototype.some.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title => - title.textContent.trim().toLowerCase() === 'frequently used' + const hasFrequentlyUsedHeading = Array.prototype.some.call( + emojiMenu.querySelectorAll('.emoji-menu-title'), + title => title.textContent.trim().toLowerCase() === 'frequently used', ); expect(hasFrequentlyUsedHeading).toBe(true); }) .then(done) - .catch((err) => { + .catch(err => { done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); @@ -361,4 +379,4 @@ import '~/lib/utils/common_utils'; }); }); }); -}).call(window); +}.call(window)); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index d03836d10f9..d8aa5c636da 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -4,12 +4,11 @@ import '~/behaviors/quick_submit'; describe('Quick Submit behavior', function () { const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options); - preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + preloadFixtures('snippets/show.html.raw'); beforeEach(() => { - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - $('body').attr('data-page', 'projects:merge_requests:show'); - $('form').submit((e) => { + loadFixtures('snippets/show.html.raw'); + $('form').submit(e => { // Prevent a form submit from moving us off the testing page e.preventDefault(); }); @@ -26,24 +25,30 @@ describe('Quick Submit behavior', function () { }); it('does not respond to other keyCodes', () => { - this.textarea.trigger(keydownEvent({ - keyCode: 32, - })); + this.textarea.trigger( + keydownEvent({ + keyCode: 32, + }), + ); expect(this.spies.submit).not.toHaveBeenTriggered(); }); it('does not respond to Enter alone', () => { - this.textarea.trigger(keydownEvent({ - ctrlKey: false, - metaKey: false, - })); + this.textarea.trigger( + keydownEvent({ + ctrlKey: false, + metaKey: false, + }), + ); expect(this.spies.submit).not.toHaveBeenTriggered(); }); it('does not respond to repeated events', () => { - this.textarea.trigger(keydownEvent({ - repeat: true, - })); + this.textarea.trigger( + keydownEvent({ + repeat: true, + }), + ); expect(this.spies.submit).not.toHaveBeenTriggered(); }); @@ -83,15 +88,21 @@ describe('Quick Submit behavior', function () { }); it('excludes other modifier keys', () => { - this.textarea.trigger(keydownEvent({ - altKey: true, - })); - this.textarea.trigger(keydownEvent({ - ctrlKey: true, - })); - this.textarea.trigger(keydownEvent({ - shiftKey: true, - })); + this.textarea.trigger( + keydownEvent({ + altKey: true, + }), + ); + this.textarea.trigger( + keydownEvent({ + ctrlKey: true, + }), + ); + this.textarea.trigger( + keydownEvent({ + shiftKey: true, + }), + ); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); }); @@ -102,15 +113,21 @@ describe('Quick Submit behavior', function () { }); it('excludes other modifier keys', () => { - this.textarea.trigger(keydownEvent({ - altKey: true, - })); - this.textarea.trigger(keydownEvent({ - metaKey: true, - })); - this.textarea.trigger(keydownEvent({ - shiftKey: true, - })); + this.textarea.trigger( + keydownEvent({ + altKey: true, + }), + ); + this.textarea.trigger( + keydownEvent({ + metaKey: true, + }), + ); + this.textarea.trigger( + keydownEvent({ + shiftKey: true, + }), + ); return expect(this.spies.submit).not.toHaveBeenTriggered(); }); } diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/app_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/changed_files_dropdown_spec.js b/spec/javascripts/diffs/components/changed_files_dropdown_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/changed_files_dropdown_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/changed_files_spec.js b/spec/javascripts/diffs/components/changed_files_spec.js new file mode 100644 index 00000000000..2d57af6137c --- /dev/null +++ b/spec/javascripts/diffs/components/changed_files_spec.js @@ -0,0 +1,100 @@ +import Vue from 'vue'; +import $ from 'jquery'; +import { mountComponentWithStore } from 'spec/helpers'; +import store from '~/diffs/store'; +import ChangedFiles from '~/diffs/components/changed_files.vue'; + +describe('ChangedFiles', () => { + const Component = Vue.extend(ChangedFiles); + const createComponent = props => mountComponentWithStore(Component, { props, store }); + let vm; + + beforeEach(() => { + setFixtures(` + <div id="dummy-element"></div> + <div class="js-tabs-affix"></div> + `); + const props = { + diffFiles: [ + { + addedLines: 10, + removedLines: 20, + blob: { + path: 'some/code.txt', + }, + filePath: 'some/code.txt', + }, + ], + }; + vm = createComponent(props); + }); + + describe('with single file added', () => { + it('shows files changes', () => { + expect(vm.$el).toContainText('1 changed file'); + }); + + it('shows file additions and deletions', () => { + expect(vm.$el).toContainText('10 additions'); + expect(vm.$el).toContainText('20 deletions'); + }); + }); + + describe('template', () => { + describe('diff view mode buttons', () => { + let inlineButton; + let parallelButton; + + beforeEach(() => { + inlineButton = vm.$el.querySelector('.js-inline-diff-button'); + parallelButton = vm.$el.querySelector('.js-parallel-diff-button'); + }); + + it('should have Inline and Side-by-side buttons', () => { + expect(inlineButton).toBeDefined(); + expect(parallelButton).toBeDefined(); + }); + + it('should add active class to Inline button', done => { + vm.$store.state.diffs.diffViewType = 'inline'; + + vm.$nextTick(() => { + expect(inlineButton.classList.contains('active')).toEqual(true); + expect(parallelButton.classList.contains('active')).toEqual(false); + + done(); + }); + }); + + it('should toggle active state of buttons when diff view type changed', done => { + vm.$store.state.diffs.diffViewType = 'parallel'; + + vm.$nextTick(() => { + expect(inlineButton.classList.contains('active')).toEqual(false); + expect(parallelButton.classList.contains('active')).toEqual(true); + + done(); + }); + }); + + describe('clicking them', () => { + it('should toggle the diff view type', done => { + $(parallelButton).click(); + + vm.$nextTick(() => { + expect(inlineButton.classList.contains('active')).toEqual(false); + expect(parallelButton.classList.contains('active')).toEqual(true); + + $(inlineButton).click(); + + vm.$nextTick(() => { + expect(inlineButton.classList.contains('active')).toEqual(true); + expect(parallelButton.classList.contains('active')).toEqual(false); + done(); + }); + }); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/diff_content_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js new file mode 100644 index 00000000000..270f363825f --- /dev/null +++ b/spec/javascripts/diffs/components/diff_discussions_spec.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import DiffDiscussions from '~/diffs/components/diff_discussions.vue'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import discussionsMockData from '../mock_data/diff_discussions'; + +describe('DiffDiscussions', () => { + let component; + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + + beforeEach(() => { + component = createComponentWithStore(Vue.extend(DiffDiscussions), store, { + discussions: getDiscussionsMockData(), + }).$mount(); + }); + + describe('template', () => { + it('should have notes list', () => { + const { $el } = component; + + expect($el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js new file mode 100644 index 00000000000..d0f1700bee6 --- /dev/null +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -0,0 +1,433 @@ +import Vue from 'vue'; +import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +const discussionFixture = 'merge_requests/diff_discussion.json'; + +describe('diff_file_header', () => { + let vm; + let props; + const Component = Vue.extend(DiffFileHeader); + + beforeEach(() => { + const diffDiscussionMock = getJSONFixture(discussionFixture)[0]; + const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file, { deep: true }); + props = { + diffFile, + currentUser: {}, + }; + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('icon', () => { + beforeEach(() => { + props.diffFile.blob.icon = 'dummy icon'; + }); + + it('returns the blob icon for files', () => { + props.diffFile.submodule = false; + + vm = mountComponent(Component, props); + + expect(vm.icon).toBe(props.diffFile.blob.icon); + }); + + it('returns the archive icon for submodules', () => { + props.diffFile.submodule = true; + + vm = mountComponent(Component, props); + + expect(vm.icon).toBe('archive'); + }); + }); + + describe('titleLink', () => { + beforeEach(() => { + Object.assign(props.diffFile, { + fileHash: 'badc0ffee', + submoduleLink: 'link://to/submodule', + submoduleTreeUrl: 'some://tree/url', + }); + }); + + it('returns the fileHash for files', () => { + props.diffFile.submodule = false; + + vm = mountComponent(Component, props); + + expect(vm.titleLink).toBe(`#${props.diffFile.fileHash}`); + }); + + it('returns the submoduleTreeUrl for submodules', () => { + props.diffFile.submodule = true; + + vm = mountComponent(Component, props); + + expect(vm.titleLink).toBe(props.diffFile.submoduleTreeUrl); + }); + + it('returns the submoduleLink for submodules without submoduleTreeUrl', () => { + Object.assign(props.diffFile, { + submodule: true, + submoduleTreeUrl: null, + }); + + vm = mountComponent(Component, props); + + expect(vm.titleLink).toBe(props.diffFile.submoduleLink); + }); + }); + + describe('filePath', () => { + beforeEach(() => { + Object.assign(props.diffFile, { + blob: { id: 'b10b1db10b1d' }, + filePath: 'path/to/file', + }); + }); + + it('returns the filePath for files', () => { + props.diffFile.submodule = false; + + vm = mountComponent(Component, props); + + expect(vm.filePath).toBe(props.diffFile.filePath); + }); + + it('appends the truncated blob id for submodules', () => { + props.diffFile.submodule = true; + + vm = mountComponent(Component, props); + + expect(vm.filePath).toBe( + `${props.diffFile.filePath} @ ${props.diffFile.blob.id.substr(0, 8)}`, + ); + }); + }); + + describe('titleTag', () => { + it('returns a link tag if fileHash is set', () => { + props.diffFile.fileHash = 'some hash'; + + vm = mountComponent(Component, props); + + expect(vm.titleTag).toBe('a'); + }); + + it('returns a span tag if fileHash is not set', () => { + props.diffFile.fileHash = null; + + vm = mountComponent(Component, props); + + expect(vm.titleTag).toBe('span'); + }); + }); + + describe('isUsingLfs', () => { + beforeEach(() => { + Object.assign(props.diffFile, { + storedExternally: true, + externalStorage: 'lfs', + }); + }); + + it('returns true if file is stored in LFS', () => { + vm = mountComponent(Component, props); + + expect(vm.isUsingLfs).toBe(true); + }); + + it('returns false if file is not stored externally', () => { + props.diffFile.storedExternally = false; + + vm = mountComponent(Component, props); + + expect(vm.isUsingLfs).toBe(false); + }); + + it('returns false if file is not stored in LFS', () => { + props.diffFile.externalStorage = 'not lfs'; + + vm = mountComponent(Component, props); + + expect(vm.isUsingLfs).toBe(false); + }); + }); + + describe('collapseIcon', () => { + it('returns chevron-down if the diff is expanded', () => { + props.expanded = true; + + vm = mountComponent(Component, props); + + expect(vm.collapseIcon).toBe('chevron-down'); + }); + + it('returns chevron-right if the diff is collapsed', () => { + props.expanded = false; + + vm = mountComponent(Component, props); + + expect(vm.collapseIcon).toBe('chevron-right'); + }); + }); + + describe('isDiscussionsExpanded', () => { + beforeEach(() => { + Object.assign(props, { + discussionsExpanded: true, + expanded: true, + }); + }); + + it('returns true if diff and discussion are expanded', () => { + vm = mountComponent(Component, props); + + expect(vm.isDiscussionsExpanded).toBe(true); + }); + + it('returns false if discussion is collapsed', () => { + props.discussionsExpanded = false; + + vm = mountComponent(Component, props); + + expect(vm.isDiscussionsExpanded).toBe(false); + }); + + it('returns false if diff is collapsed', () => { + props.expanded = false; + + vm = mountComponent(Component, props); + + expect(vm.isDiscussionsExpanded).toBe(false); + }); + }); + + describe('viewFileButtonText', () => { + it('contains the truncated content SHA', () => { + const dummySha = 'deebd00f is no SHA'; + props.diffFile.contentSha = dummySha; + + vm = mountComponent(Component, props); + + expect(vm.viewFileButtonText).not.toContain(dummySha); + expect(vm.viewFileButtonText).toContain(dummySha.substr(0, 8)); + }); + }); + + describe('viewReplacedFileButtonText', () => { + it('contains the truncated base SHA', () => { + const dummySha = 'deadabba sings no more'; + props.diffFile.diffRefs.baseSha = dummySha; + + vm = mountComponent(Component, props); + + expect(vm.viewReplacedFileButtonText).not.toContain(dummySha); + expect(vm.viewReplacedFileButtonText).toContain(dummySha.substr(0, 8)); + }); + }); + }); + + describe('methods', () => { + describe('handleToggle', () => { + beforeEach(() => { + spyOn(vm, '$emit').and.stub(); + }); + + it('emits toggleFile if checkTarget is false', () => { + vm.handleToggle(null, false); + + expect(vm.$emit).toHaveBeenCalledWith('toggleFile'); + }); + + it('emits toggleFile if checkTarget is true and event target is header', () => { + vm.handleToggle({ target: vm.$refs.header }, true); + + expect(vm.$emit).toHaveBeenCalledWith('toggleFile'); + }); + + it('does not emit toggleFile if checkTarget is true and event target is not header', () => { + vm.handleToggle({ target: 'not header' }, true); + + expect(vm.$emit).not.toHaveBeenCalled(); + }); + }); + }); + + describe('template', () => { + describe('collapse toggle', () => { + const collapseToggle = () => vm.$el.querySelector('.diff-toggle-caret'); + + it('is visible if collapsible is true', () => { + props.collapsible = true; + + vm = mountComponent(Component, props); + + expect(collapseToggle()).not.toBe(null); + }); + + it('is hidden if collapsible is false', () => { + props.collapsible = false; + + vm = mountComponent(Component, props); + + expect(collapseToggle()).toBe(null); + }); + }); + + it('displays an icon in the title', () => { + vm = mountComponent(Component, props); + + const icon = vm.$el.querySelector(`i[class="fa fa-fw fa-${vm.icon}"]`); + expect(icon).not.toBe(null); + }); + + describe('file paths', () => { + const filePaths = () => vm.$el.querySelectorAll('.file-title-name'); + + it('displays the path of a added file', () => { + props.diffFile.renamedFile = false; + + vm = mountComponent(Component, props); + + expect(filePaths()).toHaveLength(1); + expect(filePaths()[0]).toHaveText(props.diffFile.filePath); + }); + + it('displays path for deleted file', () => { + props.diffFile.renamedFile = false; + props.diffFile.deletedFile = true; + + vm = mountComponent(Component, props); + + expect(filePaths()).toHaveLength(1); + expect(filePaths()[0]).toHaveText(`${props.diffFile.filePath} deleted`); + }); + + it('displays old and new path if the file was renamed', () => { + props.diffFile.renamedFile = true; + + vm = mountComponent(Component, props); + + expect(filePaths()).toHaveLength(2); + expect(filePaths()[0]).toHaveText(props.diffFile.oldPath); + expect(filePaths()[1]).toHaveText(props.diffFile.newPath); + }); + }); + + it('displays a copy to clipboard button', () => { + vm = mountComponent(Component, props); + + const button = vm.$el.querySelector('.btn-clipboard'); + expect(button).not.toBe(null); + expect(button.dataset.clipboardText).toBe(props.diffFile.filePath); + }); + + describe('file mode', () => { + it('it displays old and new file mode if it changed', () => { + props.diffFile.modeChanged = true; + + vm = mountComponent(Component, props); + + const { fileMode } = vm.$refs; + expect(fileMode).not.toBe(undefined); + expect(fileMode).toContainText(props.diffFile.aMode); + expect(fileMode).toContainText(props.diffFile.bMode); + }); + + it('does not display the file mode if it has not changed', () => { + props.diffFile.modeChanged = false; + + vm = mountComponent(Component, props); + + const { fileMode } = vm.$refs; + expect(fileMode).toBe(undefined); + }); + }); + + describe('LFS label', () => { + const lfsLabel = () => vm.$el.querySelector('.label-lfs'); + + it('displays the LFS label for files stored in LFS', () => { + Object.assign(props.diffFile, { + storedExternally: true, + externalStorage: 'lfs', + }); + + vm = mountComponent(Component, props); + + expect(lfsLabel()).not.toBe(null); + expect(lfsLabel()).toHaveText('LFS'); + }); + + it('does not display the LFS label for files stored in repository', () => { + props.diffFile.storedExternally = false; + + vm = mountComponent(Component, props); + + expect(lfsLabel()).toBe(null); + }); + }); + + describe('edit button', () => { + it('should not render edit button if addMergeRequestButtons is not true', () => { + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null); + }); + + it('should show edit button when file is editable', () => { + props.addMergeRequestButtons = true; + props.diffFile.editPath = '/'; + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector('.js-edit-blob')).toContainText('Edit'); + }); + + it('should not show edit button when file is deleted', () => { + props.addMergeRequestButtons = true; + props.diffFile.deletedFile = true; + props.diffFile.editPath = '/'; + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null); + }); + }); + + describe('addMergeRequestButtons', () => { + beforeEach(() => { + props.addMergeRequestButtons = true; + props.diffFile.editPath = ''; + }); + + describe('view on environment button', () => { + const url = 'some.external.url/'; + const title = 'url.title'; + + it('displays link to external url', () => { + props.diffFile.externalUrl = url; + props.diffFile.formattedExternalUrl = title; + + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector(`a[href="${url}"]`)).not.toBe(null); + expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).not.toBe(null); + }); + + it('hides link if no external url', () => { + props.diffFile.externalUrl = ''; + props.diffFile.formattedExternalUrl = title; + + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).toBe(null); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js new file mode 100644 index 00000000000..1c1edfac68c --- /dev/null +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; +import DiffFileComponent from '~/diffs/components/diff_file.vue'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('DiffFile', () => { + let vm; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(DiffFileComponent), store, { + file: getDiffFileMock(), + currentUser: {}, + }).$mount(); + }); + + describe('template', () => { + it('should render component with file header, file content components', () => { + const el = vm.$el; + const { fileHash, filePath } = diffFileMockData; + + expect(el.id).toEqual(fileHash); + expect(el.classList.contains('diff-file')).toEqual(true); + expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0); + expect(el.querySelector('.js-file-title')).toBeDefined(); + expect(el.querySelector('.file-title-name').innerText.indexOf(filePath) > -1).toEqual(true); + expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); + expect(el.querySelectorAll('.line_content').length > 5).toEqual(true); + }); + + describe('collapsed', () => { + it('should not have file content', done => { + expect(vm.$el.querySelectorAll('.diff-content.hidden').length).toEqual(0); + expect(vm.file.collapsed).toEqual(false); + vm.file.collapsed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('.diff-content.hidden').length).toEqual(1); + + done(); + }); + }); + + it('should have collapsed text and link', done => { + vm.file.collapsed = true; + + vm.$nextTick(() => { + expect(vm.$el.innerText).toContain('This diff is collapsed'); + expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1); + + done(); + }); + }); + + it('should have loading icon while loading a collapsed diffs', done => { + vm.file.collapsed = true; + vm.isLoadingCollapsedDiff = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('.diff-content.loading').length).toEqual(1); + + done(); + }); + }); + }); + }); + + describe('too large diff', () => { + it('should have too large warning and blob link', done => { + const BLOB_LINK = '/file/view/path'; + vm.file.tooLarge = true; + vm.file.viewPath = BLOB_LINK; + + vm.$nextTick(() => { + expect(vm.$el.innerText).toContain( + 'This source diff could not be displayed because it is too large', + ); + expect(vm.$el.querySelector('.js-too-large-diff')).toBeDefined(); + expect(vm.$el.querySelector('.js-too-large-diff a').href.indexOf(BLOB_LINK) > -1).toEqual( + true, + ); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js new file mode 100644 index 00000000000..0085a16815a --- /dev/null +++ b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js @@ -0,0 +1,115 @@ +import Vue from 'vue'; +import DiffGutterAvatarsComponent from '~/diffs/components/diff_gutter_avatars.vue'; +import { COUNT_OF_AVATARS_IN_GUTTER } from '~/diffs/constants'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import discussionsMockData from '../mock_data/diff_discussions'; + +describe('DiffGutterAvatars', () => { + let component; + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + + beforeEach(() => { + component = createComponentWithStore(Vue.extend(DiffGutterAvatarsComponent), store, { + discussions: getDiscussionsMockData(), + }).$mount(); + }); + + describe('computed', () => { + describe('discussionsExpanded', () => { + it('should return true when all discussions are expanded', () => { + expect(component.discussionsExpanded).toEqual(true); + }); + + it('should return false when all discussions are not expanded', () => { + component.discussions[0].expanded = false; + expect(component.discussionsExpanded).toEqual(false); + }); + }); + + describe('allDiscussions', () => { + it('should return an array of notes', () => { + expect(component.allDiscussions).toEqual([...component.discussions[0].notes]); + }); + }); + + describe('notesInGutter', () => { + it('should return a subset of discussions to show in gutter', () => { + expect(component.notesInGutter.length).toEqual(COUNT_OF_AVATARS_IN_GUTTER); + expect(component.notesInGutter[0]).toEqual({ + note: component.discussions[0].notes[0].note, + author: component.discussions[0].notes[0].author, + }); + }); + }); + + describe('moreCount', () => { + it('should return count of remaining discussions from gutter', () => { + expect(component.moreCount).toEqual(2); + }); + }); + + describe('moreText', () => { + it('should return proper text if moreCount > 0', () => { + expect(component.moreText).toEqual('2 more comments'); + }); + + it('should return empty string if there is no discussion', () => { + component.discussions = []; + expect(component.moreText).toEqual(''); + }); + }); + }); + + describe('methods', () => { + describe('getTooltipText', () => { + it('should return original comment if it is shorter than max length', () => { + const note = component.discussions[0].notes[0]; + + expect(component.getTooltipText(note)).toEqual('Administrator: comment 1'); + }); + + it('should return truncated version of comment', () => { + const note = component.discussions[0].notes[1]; + + expect(component.getTooltipText(note)).toEqual('Fatih Acet: comment 2 is r...'); + }); + }); + + describe('toggleDiscussions', () => { + it('should toggle all discussions', () => { + expect(component.discussions[0].expanded).toEqual(true); + + component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); + component.discussions = component.$store.state.notes.discussions; + component.toggleDiscussions(); + + expect(component.discussions[0].expanded).toEqual(false); + component.$store.dispatch('setInitialNotes', []); + }); + }); + }); + + describe('template', () => { + const buttonSelector = '.js-diff-comment-button'; + const svgSelector = `${buttonSelector} svg`; + const avatarSelector = '.js-diff-comment-avatar'; + const plusCountSelector = '.js-diff-comment-plus'; + + it('should have button to collapse discussions when the discussions expanded', () => { + expect(component.$el.querySelector(buttonSelector)).toBeDefined(); + expect(component.$el.querySelector(svgSelector)).toBeDefined(); + }); + + it('should have user avatars when discussions collapsed', () => { + component.discussions[0].expanded = false; + + Vue.nextTick(() => { + expect(component.$el.querySelector(buttonSelector)).toBeNull(); + expect(component.$el.querySelectorAll(avatarSelector).length).toEqual(4); + expect(component.$el.querySelector(plusCountSelector)).toBeDefined(); + expect(component.$el.querySelector(plusCountSelector).textContent).toEqual('+2'); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js new file mode 100644 index 00000000000..312a684f4d2 --- /dev/null +++ b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js @@ -0,0 +1,153 @@ +import Vue from 'vue'; +import DiffLineGutterContent from '~/diffs/components/diff_line_gutter_content.vue'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { + MATCH_LINE_TYPE, + CONTEXT_LINE_TYPE, + OLD_NO_NEW_LINE_TYPE, + NEW_NO_NEW_LINE_TYPE, +} from '~/diffs/constants'; +import discussionsMockData from '../mock_data/diff_discussions'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('DiffLineGutterContent', () => { + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + const createComponent = (options = {}) => { + const cmp = Vue.extend(DiffLineGutterContent); + const props = Object.assign({}, options); + props.fileHash = getDiffFileMock().fileHash; + props.contextLinesPath = '/context/lines/path'; + + return createComponentWithStore(cmp, store, props).$mount(); + }; + const setDiscussions = component => { + component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); + }; + + const resetDiscussions = component => { + component.$store.dispatch('setInitialNotes', []); + }; + + describe('computed', () => { + describe('isMatchLine', () => { + it('should return true for match line type', () => { + const component = createComponent({ lineType: MATCH_LINE_TYPE }); + expect(component.isMatchLine).toEqual(true); + }); + + it('should return false for non-match line type', () => { + const component = createComponent({ lineType: CONTEXT_LINE_TYPE }); + expect(component.isMatchLine).toEqual(false); + }); + }); + + describe('isContextLine', () => { + it('should return true for context line type', () => { + const component = createComponent({ lineType: CONTEXT_LINE_TYPE }); + expect(component.isContextLine).toEqual(true); + }); + + it('should return false for non-context line type', () => { + const component = createComponent({ lineType: MATCH_LINE_TYPE }); + expect(component.isContextLine).toEqual(false); + }); + }); + + describe('isMetaLine', () => { + it('should return true for meta line type', () => { + const component = createComponent({ lineType: NEW_NO_NEW_LINE_TYPE }); + expect(component.isMetaLine).toEqual(true); + + const component2 = createComponent({ lineType: OLD_NO_NEW_LINE_TYPE }); + expect(component2.isMetaLine).toEqual(true); + }); + + it('should return false for non-meta line type', () => { + const component = createComponent({ lineType: MATCH_LINE_TYPE }); + expect(component.isMetaLine).toEqual(false); + }); + }); + + describe('lineHref', () => { + it('should prepend # to lineCode', () => { + const lineCode = 'LC_42'; + const component = createComponent({ lineCode }); + expect(component.lineHref).toEqual(`#${lineCode}`); + }); + + it('should return # if there is no lineCode', () => { + const component = createComponent({ lineCode: null }); + expect(component.lineHref).toEqual('#'); + }); + }); + + describe('discussions, hasDiscussions, shouldShowAvatarsOnGutter', () => { + it('should return empty array when there is no discussion', () => { + const component = createComponent({ lineCode: 'LC_42' }); + expect(component.discussions).toEqual([]); + expect(component.hasDiscussions).toEqual(false); + expect(component.shouldShowAvatarsOnGutter).toEqual(false); + }); + + it('should return discussions for the given lineCode', () => { + const lineCode = getDiffFileMock().highlightedDiffLines[1].lineCode; + const component = createComponent({ lineCode, showCommentButton: true }); + + setDiscussions(component); + + expect(component.discussions).toEqual(getDiscussionsMockData()); + expect(component.hasDiscussions).toEqual(true); + expect(component.shouldShowAvatarsOnGutter).toEqual(true); + + resetDiscussions(component); + }); + }); + }); + + describe('template', () => { + it('should render three dots for context lines', () => { + const component = createComponent({ + lineType: MATCH_LINE_TYPE, + }); + + expect(component.$el.querySelector('span').classList.contains('context-cell')).toEqual(true); + expect(component.$el.innerText).toEqual('...'); + }); + + it('should render comment button', () => { + const component = createComponent({ + showCommentButton: true, + }); + Object.defineProperty(component, 'isLoggedIn', { + get() { + return true; + }, + }); + + expect(component.$el.querySelector('.js-add-diff-note-button')).toBeDefined(); + }); + + it('should render line link', () => { + const lineNumber = 42; + const lineCode = `LC_${lineNumber}`; + const component = createComponent({ lineNumber, lineCode }); + const link = component.$el.querySelector('a'); + + expect(link.href.indexOf(`#${lineCode}`) > -1).toEqual(true); + expect(link.dataset.linenumber).toEqual(lineNumber.toString()); + }); + + it('should render user avatars', () => { + const component = createComponent({ + showCommentButton: true, + lineCode: getDiffFileMock().highlightedDiffLines[1].lineCode, + }); + + setDiscussions(component); + expect(component.$el.querySelector('.diff-comment-avatar-holders')).toBeDefined(); + resetDiscussions(component); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js new file mode 100644 index 00000000000..724d1948214 --- /dev/null +++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; +import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('DiffLineNoteForm', () => { + let component; + let diffFile; + let diffLines; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + + beforeEach(() => { + diffFile = getDiffFileMock(); + diffLines = diffFile.highlightedDiffLines; + + component = createComponentWithStore(Vue.extend(DiffLineNoteForm), store, { + diffFile, + diffLines, + line: diffLines[0], + noteTargetLine: diffLines[0], + }).$mount(); + }); + + describe('methods', () => { + describe('handleCancelCommentForm', () => { + it('should call cancelCommentForm with lineCode', () => { + spyOn(component, 'cancelCommentForm'); + component.handleCancelCommentForm(); + + expect(component.cancelCommentForm).toHaveBeenCalledWith({ + lineCode: diffLines[0].lineCode, + }); + }); + }); + + describe('saveNoteForm', () => { + it('should call saveNote action with proper params', done => { + let isPromiseCalled = false; + const formDataSpy = spyOnDependency(DiffLineNoteForm, 'getNoteFormData').and.returnValue({ + postData: 1, + }); + const saveNoteSpy = spyOn(component, 'saveNote').and.returnValue( + new Promise(() => { + isPromiseCalled = true; + done(); + }), + ); + + component.handleSaveNote('note body'); + + expect(formDataSpy).toHaveBeenCalled(); + expect(saveNoteSpy).toHaveBeenCalled(); + expect(isPromiseCalled).toEqual(true); + }); + }); + }); + + describe('template', () => { + it('should have note form', () => { + const { $el } = component; + + expect($el.querySelector('.js-vue-textarea')).toBeDefined(); + expect($el.querySelector('.js-vue-issue-save')).toBeDefined(); + expect($el.querySelector('.js-vue-markdown-field')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/edit_button_spec.js b/spec/javascripts/diffs/components/edit_button_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/edit_button_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/hidden_files_warning_spec.js b/spec/javascripts/diffs/components/hidden_files_warning_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/hidden_files_warning_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js new file mode 100644 index 00000000000..0d5a3576204 --- /dev/null +++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js @@ -0,0 +1,111 @@ +import Vue from 'vue'; +import InlineDiffView from '~/diffs/components/inline_diff_view.vue'; +import store from '~/mr_notes/stores'; +import * as constants from '~/diffs/constants'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; +import discussionsMockData from '../mock_data/diff_discussions'; + +describe('InlineDiffView', () => { + let component; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + + beforeEach(() => { + const diffFile = getDiffFileMock(); + + component = createComponentWithStore(Vue.extend(InlineDiffView), store, { + diffFile, + diffLines: diffFile.highlightedDiffLines, + }).$mount(); + }); + + describe('methods', () => { + describe('handleMouse', () => { + it('should set hoveredLineCode', () => { + expect(component.hoveredLineCode).toEqual(null); + + component.handleMouse('lineCode1', true); + expect(component.hoveredLineCode).toEqual('lineCode1'); + + component.handleMouse('lineCode1', false); + expect(component.hoveredLineCode).toEqual(null); + }); + }); + + describe('getLineClass', () => { + it('should return line class object', () => { + const { LINE_HOVER_CLASS_NAME, LINE_UNFOLD_CLASS_NAME } = constants; + const { MATCH_LINE_TYPE, NEW_LINE_TYPE } = constants; + + expect(component.getLineClass(component.diffLines[0])).toEqual({ + [NEW_LINE_TYPE]: NEW_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: false, + [LINE_HOVER_CLASS_NAME]: false, + }); + + component.handleMouse(component.diffLines[0].lineCode, true); + Object.defineProperty(component, 'isLoggedIn', { + get() { + return true; + }, + }); + + expect(component.getLineClass(component.diffLines[0])).toEqual({ + [NEW_LINE_TYPE]: NEW_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: false, + [LINE_HOVER_CLASS_NAME]: true, + }); + + expect(component.getLineClass(component.diffLines[5])).toEqual({ + [MATCH_LINE_TYPE]: MATCH_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: true, + [LINE_HOVER_CLASS_NAME]: false, + }); + }); + }); + }); + + describe('template', () => { + it('should have rendered diff lines', () => { + const el = component.$el; + + expect(el.querySelectorAll('tr.line_holder').length).toEqual(6); + expect(el.querySelectorAll('tr.line_holder.new').length).toEqual(2); + expect(el.querySelectorAll('tr.line_holder.match').length).toEqual(1); + expect(el.textContent.indexOf('Bad dates') > -1).toEqual(true); + }); + + it('should render discussions', done => { + const el = component.$el; + component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); + + Vue.nextTick(() => { + expect(el.querySelectorAll('.notes_holder').length).toEqual(1); + expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5); + expect(el.innerText.indexOf('comment 5') > -1).toEqual(true); + component.$store.dispatch('setInitialNotes', []); + + done(); + }); + }); + + it('should render new discussion forms', done => { + const el = component.$el; + const lines = getDiffFileMock().highlightedDiffLines; + + component.handleShowCommentForm({ lineCode: lines[0].lineCode }); + component.handleShowCommentForm({ lineCode: lines[1].lineCode }); + + Vue.nextTick(() => { + expect(el.querySelectorAll('.js-vue-markdown-field').length).toEqual(2); + expect(el.querySelectorAll('tr')[1].classList.contains('notes_holder')).toEqual(true); + expect(el.querySelectorAll('tr')[3].classList.contains('notes_holder')).toEqual(true); + + store.state.diffs.diffLineCommentForms = {}; + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/no_changes_spec.js b/spec/javascripts/diffs/components/no_changes_spec.js new file mode 100644 index 00000000000..7237274eb43 --- /dev/null +++ b/spec/javascripts/diffs/components/no_changes_spec.js @@ -0,0 +1 @@ +// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 diff --git a/spec/javascripts/diffs/components/parallel_diff_view_spec.js b/spec/javascripts/diffs/components/parallel_diff_view_spec.js new file mode 100644 index 00000000000..cab533217c0 --- /dev/null +++ b/spec/javascripts/diffs/components/parallel_diff_view_spec.js @@ -0,0 +1,224 @@ +import Vue from 'vue'; +import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue'; +import store from '~/mr_notes/stores'; +import * as constants from '~/diffs/constants'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; +import discussionsMockData from '../mock_data/diff_discussions'; + +describe('ParallelDiffView', () => { + let component; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + + beforeEach(() => { + const diffFile = getDiffFileMock(); + + component = createComponentWithStore(Vue.extend(ParallelDiffView), store, { + diffFile, + diffLines: diffFile.parallelDiffLines, + }).$mount(); + }); + + describe('computed', () => { + describe('parallelDiffLines', () => { + it('should normalize lines for empty cells', () => { + expect(component.parallelDiffLines[0].left.type).toEqual(constants.EMPTY_CELL_TYPE); + expect(component.parallelDiffLines[1].left.type).toEqual(constants.EMPTY_CELL_TYPE); + }); + }); + }); + + describe('methods', () => { + describe('hasDiscussion', () => { + it('it should return true if there is a discussion either for left or right section', () => { + Object.defineProperty(component, 'discussionsByLineCode', { + get() { + return { line_42: true }; + }, + }); + + expect(component.hasDiscussion({ left: {}, right: {} })).toEqual(undefined); + expect(component.hasDiscussion({ left: { lineCode: 'line_42' }, right: {} })).toEqual(true); + expect(component.hasDiscussion({ left: {}, right: { lineCode: 'line_42' } })).toEqual(true); + }); + }); + + describe('getClassName', () => { + it('should return line class object', () => { + const { LINE_HOVER_CLASS_NAME, LINE_UNFOLD_CLASS_NAME } = constants; + const { MATCH_LINE_TYPE, NEW_LINE_TYPE, LINE_POSITION_RIGHT } = constants; + + expect(component.getClassName(component.diffLines[1], LINE_POSITION_RIGHT)).toEqual({ + [NEW_LINE_TYPE]: NEW_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: false, + [LINE_HOVER_CLASS_NAME]: false, + }); + + const eventMock = { + target: component.$refs.rightLines[1], + }; + + component.handleMouse(eventMock, component.diffLines[1], true); + Object.defineProperty(component, 'isLoggedIn', { + get() { + return true; + }, + }); + + expect(component.getClassName(component.diffLines[1], LINE_POSITION_RIGHT)).toEqual({ + [NEW_LINE_TYPE]: NEW_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: false, + [LINE_HOVER_CLASS_NAME]: true, + }); + + expect(component.getClassName(component.diffLines[5], LINE_POSITION_RIGHT)).toEqual({ + [MATCH_LINE_TYPE]: MATCH_LINE_TYPE, + [LINE_UNFOLD_CLASS_NAME]: true, + [LINE_HOVER_CLASS_NAME]: false, + }); + }); + }); + + describe('handleMouse', () => { + it('should set hovered line code and line section to null when isHover is false', () => { + const rightLineEventMock = { target: component.$refs.rightLines[1] }; + expect(component.hoveredLineCode).toEqual(null); + expect(component.hoveredSection).toEqual(null); + + component.handleMouse(rightLineEventMock, null, false); + expect(component.hoveredLineCode).toEqual(null); + expect(component.hoveredSection).toEqual(null); + }); + + it('should set hovered line code and line section for right section', () => { + const rightLineEventMock = { target: component.$refs.rightLines[1] }; + component.handleMouse(rightLineEventMock, component.diffLines[1], true); + expect(component.hoveredLineCode).toEqual(component.diffLines[1].right.lineCode); + expect(component.hoveredSection).toEqual(constants.LINE_POSITION_RIGHT); + }); + + it('should set hovered line code and line section for left section', () => { + const leftLineEventMock = { target: component.$refs.leftLines[2] }; + component.handleMouse(leftLineEventMock, component.diffLines[2], true); + expect(component.hoveredLineCode).toEqual(component.diffLines[2].left.lineCode); + expect(component.hoveredSection).toEqual(constants.LINE_POSITION_LEFT); + }); + }); + + describe('shouldRenderDiscussions', () => { + it('should return true if there is a discussion on left side and it is expanded', () => { + const line = { left: { lineCode: 'lineCode1' } }; + spyOn(component, 'isDiscussionExpanded').and.returnValue(true); + Object.defineProperty(component, 'discussionsByLineCode', { + get() { + return { + [line.left.lineCode]: true, + }; + }, + }); + + expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_LEFT)).toEqual(true); + expect(component.isDiscussionExpanded).toHaveBeenCalledWith(line.left.lineCode); + }); + + it('should return false if there is a discussion on left side but it is collapsed', () => { + const line = { left: { lineCode: 'lineCode1' } }; + spyOn(component, 'isDiscussionExpanded').and.returnValue(false); + Object.defineProperty(component, 'discussionsByLineCode', { + get() { + return { + [line.left.lineCode]: true, + }; + }, + }); + + expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_LEFT)).toEqual( + false, + ); + }); + + it('should return false for discussions on the right side if there is no line type', () => { + const CUSTOM_RIGHT_LINE_TYPE = 'CUSTOM_RIGHT_LINE_TYPE'; + const line = { right: { lineCode: 'lineCode1', type: CUSTOM_RIGHT_LINE_TYPE } }; + spyOn(component, 'isDiscussionExpanded').and.returnValue(true); + Object.defineProperty(component, 'discussionsByLineCode', { + get() { + return { + [line.right.lineCode]: true, + }; + }, + }); + + expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_RIGHT)).toEqual( + CUSTOM_RIGHT_LINE_TYPE, + ); + }); + }); + + describe('hasAnyExpandedDiscussion', () => { + const LINE_CODE_LEFT = 'LINE_CODE_LEFT'; + const LINE_CODE_RIGHT = 'LINE_CODE_RIGHT'; + + it('should return true if there is a discussion either on the left or the right side', () => { + const mockLineOne = { + right: { lineCode: LINE_CODE_RIGHT }, + left: {}, + }; + const mockLineTwo = { + left: { lineCode: LINE_CODE_LEFT }, + right: {}, + }; + + spyOn(component, 'isDiscussionExpanded').and.callFake(lc => lc === LINE_CODE_RIGHT); + expect(component.hasAnyExpandedDiscussion(mockLineOne)).toEqual(true); + expect(component.hasAnyExpandedDiscussion(mockLineTwo)).toEqual(false); + }); + }); + }); + + describe('template', () => { + it('should have rendered diff lines', () => { + const el = component.$el; + + expect(el.querySelectorAll('tr.line_holder.parallel').length).toEqual(6); + expect(el.querySelectorAll('td.empty-cell').length).toEqual(4); + expect(el.querySelectorAll('td.line_content.parallel.right-side').length).toEqual(6); + expect(el.querySelectorAll('td.line_content.parallel.left-side').length).toEqual(6); + expect(el.querySelectorAll('td.match').length).toEqual(4); + expect(el.textContent.indexOf('Bad dates') > -1).toEqual(true); + }); + + it('should render discussions', done => { + const el = component.$el; + component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); + + Vue.nextTick(() => { + expect(el.querySelectorAll('.notes_holder').length).toEqual(1); + expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5); + expect(el.innerText.indexOf('comment 5') > -1).toEqual(true); + component.$store.dispatch('setInitialNotes', []); + + done(); + }); + }); + + it('should render new discussion forms', done => { + const el = component.$el; + const lines = getDiffFileMock().parallelDiffLines; + + component.handleShowCommentForm({ lineCode: lines[0].lineCode }); + component.handleShowCommentForm({ lineCode: lines[1].lineCode }); + + Vue.nextTick(() => { + expect(el.querySelectorAll('.js-vue-markdown-field').length).toEqual(2); + expect(el.querySelectorAll('tr')[1].classList.contains('notes_holder')).toEqual(true); + expect(el.querySelectorAll('tr')[3].classList.contains('notes_holder')).toEqual(true); + + store.state.diffs.diffLineCommentForms = {}; + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js new file mode 100644 index 00000000000..41d0dfd8939 --- /dev/null +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -0,0 +1,496 @@ +export default { + id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + reply_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + position: { + formatter: { + old_line: null, + new_line: 2, + old_path: 'CHANGELOG', + new_path: 'CHANGELOG', + base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a', + start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962', + head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + }, + }, + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + expanded: true, + notes: [ + { + id: 1749, + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-03T21:06:21.521Z', + updated_at: '2018-04-08T08:50:41.762Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 1', + note_html: '<p dir="auto">comment 1</p>', + last_edited_at: '2018-04-08T08:50:41.762Z', + last_edited_by: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1749/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1749&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1749', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1749', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: 1753, + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Fatih Acet', + username: 'fatihacet', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/fatihacevt', + }, + created_at: '2018-04-08T08:49:35.804Z', + updated_at: '2018-04-08T08:50:45.915Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 2 is really long one', + note_html: '<p dir="auto">comment 2 is really long one</p>', + last_edited_at: '2018-04-08T08:50:45.915Z', + last_edited_by: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1753/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1753&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1753', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1753', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: 1754, + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-08T08:50:48.294Z', + updated_at: '2018-04-08T08:50:48.294Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 3', + note_html: '<p dir="auto">comment 3</p>', + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1754/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1754&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1754', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1754', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: 1755, + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-08T08:50:50.911Z', + updated_at: '2018-04-08T08:50:50.911Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 4', + note_html: '<p dir="auto">comment 4</p>', + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1755/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1755&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1755', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1755', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: 1756, + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-08T08:50:53.895Z', + updated_at: '2018-04-08T08:50:53.895Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 5', + note_html: '<p dir="auto">comment 5</p>', + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1756/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1756&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1756', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1756', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + ], + individual_note: false, + resolvable: true, + resolved: false, + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + diff_file: { + submodule: false, + submodule_link: null, + blob: { + id: '9e10516ca50788acf18c518a231914a21e5f16f7', + path: 'CHANGELOG', + name: 'CHANGELOG', + mode: '100644', + readable_text: true, + icon: 'file-text-o', + }, + blob_path: 'CHANGELOG', + blob_name: 'CHANGELOG', + blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>', + file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', + file_path: 'CHANGELOG', + new_file: false, + deleted_file: false, + renamed_file: false, + old_path: 'CHANGELOG', + new_path: 'CHANGELOG', + mode_changed: false, + a_mode: '100644', + b_mode: '100644', + text: true, + added_lines: 2, + removed_lines: 0, + diff_refs: { + base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a', + start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962', + head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + }, + content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + stored_externally: null, + external_storage: null, + old_path_html: ['CHANGELOG', 'CHANGELOG'], + new_path_html: 'CHANGELOG', + context_lines_path: + '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff', + highlighted_diff_lines: [ + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + old_line: null, + new_line: 1, + text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + old_line: null, + new_line: 2, + text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + old_line: 1, + new_line: 3, + text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + old_line: 2, + new_line: 4, + text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + old_line: 3, + new_line: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + meta_data: null, + }, + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + ], + parallel_diff_lines: [ + { + left: null, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + old_line: null, + new_line: 1, + text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + meta_data: null, + }, + }, + { + left: null, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + old_line: null, + new_line: 2, + text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + old_line: 1, + new_line: 3, + text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + meta_data: null, + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + old_line: 1, + new_line: 3, + text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + old_line: 2, + new_line: 4, + text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + old_line: 2, + new_line: 4, + text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + old_line: 3, + new_line: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + meta_data: null, + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + old_line: 3, + new_line: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + right: { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + }, + ], + }, + diff_discussion: true, + truncated_diff_lines: + '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new noteable_line"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new noteable_line"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n', + image_diff_html: + '<div class="image js-replaced-image" data="">\n<div class="two-up view">\n<div class="wrap">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n<div class="wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n</div>\n<div class="swipe view hide">\n<div class="swipe-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="swipe-wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n</div>\n<span class="swipe-bar">\n<span class="top-handle"></span>\n<span class="bottom-handle"></span>\n</span>\n</div>\n</div>\n<div class="onion-skin view hide">\n<div class="onion-skin-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<div class="controls">\n<div class="transparent"></div>\n<div class="drag-track">\n<div class="dragger" style="left: 0px;"></div>\n</div>\n<div class="opaque"></div>\n</div>\n</div>\n</div>\n</div>\n<div class="view-modes hide">\n<ul class="view-modes-menu">\n<li class="two-up" data-mode="two-up">2-up</li>\n<li class="swipe" data-mode="swipe">Swipe</li>\n<li class="onion-skin" data-mode="onion-skin">Onion skin</li>\n</ul>\n</div>\n', +}; diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js new file mode 100644 index 00000000000..d3bf9525924 --- /dev/null +++ b/spec/javascripts/diffs/mock_data/diff_file.js @@ -0,0 +1,220 @@ +export default { + submodule: false, + submoduleLink: null, + blob: { + id: '9e10516ca50788acf18c518a231914a21e5f16f7', + path: 'CHANGELOG', + name: 'CHANGELOG', + mode: '100644', + readableText: true, + icon: 'file-text-o', + }, + blobPath: 'CHANGELOG', + blobName: 'CHANGELOG', + blobIcon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>', + fileHash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', + filePath: 'CHANGELOG', + newFile: false, + deletedFile: false, + renamedFile: false, + oldPath: 'CHANGELOG', + newPath: 'CHANGELOG', + modeChanged: false, + aMode: '100644', + bMode: '100644', + text: true, + addedLines: 2, + removedLines: 0, + diffRefs: { + baseSha: 'e63f41fe459e62e1228fcef60d7189127aeba95a', + startSha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962', + headSha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + }, + contentSha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + storedExternally: null, + externalStorage: null, + oldPathHtml: ['CHANGELOG', 'CHANGELOG'], + newPathHtml: 'CHANGELOG', + editPath: '/gitlab-org/gitlab-test/edit/spooky-stuff/CHANGELOG', + viewPath: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG', + replacedViewPath: null, + collapsed: false, + tooLarge: false, + contextLinesPath: + '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff', + highlightedDiffLines: [ + { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + oldLine: null, + newLine: 1, + text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + richText: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + metaData: null, + }, + { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + oldLine: null, + newLine: 2, + text: '+<span id="LC2" class="line" lang="plaintext"></span>\n', + richText: '+<span id="LC2" class="line" lang="plaintext"></span>\n', + metaData: null, + }, + { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + oldLine: 1, + newLine: 3, + text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + richText: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + metaData: null, + }, + { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + oldLine: 2, + newLine: 4, + text: ' <span id="LC4" class="line" lang="plaintext"></span>\n', + richText: ' <span id="LC4" class="line" lang="plaintext"></span>\n', + metaData: null, + }, + { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + oldLine: 3, + newLine: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + richText: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + metaData: null, + }, + { + lineCode: null, + type: 'match', + oldLine: null, + newLine: null, + text: '', + richText: '', + metaData: { + oldPos: 3, + newPos: 5, + }, + }, + ], + parallelDiffLines: [ + { + left: { + type: 'empty-cell', + }, + right: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + oldLine: null, + newLine: 1, + text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + richText: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + metaData: null, + }, + }, + { + left: { + type: 'empty-cell', + }, + right: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + oldLine: null, + newLine: 2, + text: '+<span id="LC2" class="line" lang="plaintext"></span>\n', + richText: '<span id="LC2" class="line" lang="plaintext"></span>\n', + metaData: null, + }, + }, + { + left: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + oldLine: 1, + newLine: 3, + text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + metaData: null, + }, + right: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + oldLine: 1, + newLine: 3, + text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + metaData: null, + }, + }, + { + left: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + oldLine: 2, + newLine: 4, + text: ' <span id="LC4" class="line" lang="plaintext"></span>\n', + richText: '<span id="LC4" class="line" lang="plaintext"></span>\n', + metaData: null, + }, + right: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + oldLine: 2, + newLine: 4, + text: ' <span id="LC4" class="line" lang="plaintext"></span>\n', + richText: '<span id="LC4" class="line" lang="plaintext"></span>\n', + metaData: null, + }, + }, + { + left: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + oldLine: 3, + newLine: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + metaData: null, + }, + right: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + oldLine: 3, + newLine: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + metaData: null, + }, + }, + { + left: { + lineCode: null, + type: 'match', + oldLine: null, + newLine: null, + text: '', + richText: '', + metaData: { + oldPos: 3, + newPos: 5, + }, + }, + right: { + lineCode: null, + type: 'match', + oldLine: null, + newLine: null, + text: '', + richText: '', + metaData: { + oldPos: 3, + newPos: 5, + }, + }, + }, + ], +}; diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js new file mode 100644 index 00000000000..e61780c9928 --- /dev/null +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -0,0 +1,210 @@ +import MockAdapter from 'axios-mock-adapter'; +import Cookies from 'js-cookie'; +import { + DIFF_VIEW_COOKIE_NAME, + INLINE_DIFF_VIEW_TYPE, + PARALLEL_DIFF_VIEW_TYPE, +} from '~/diffs/constants'; +import store from '~/diffs/store'; +import * as actions from '~/diffs/store/actions'; +import * as types from '~/diffs/store/mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import testAction from '../../helpers/vuex_action_helper'; + +describe('DiffsStoreActions', () => { + describe('setEndpoint', () => { + it('should set given endpoint', done => { + const endpoint = '/diffs/set/endpoint'; + + testAction( + actions.setEndpoint, + endpoint, + { endpoint: '' }, + [{ type: types.SET_ENDPOINT, payload: endpoint }], + [], + done, + ); + }); + }); + + describe('setLoadingState', () => { + it('should set loading state', done => { + expect(store.state.diffs.isLoading).toEqual(true); + const loadingState = false; + + testAction( + actions.setLoadingState, + loadingState, + {}, + [{ type: types.SET_LOADING, payload: loadingState }], + [], + done, + ); + }); + }); + + describe('fetchDiffFiles', () => { + it('should fetch diff files', done => { + const endpoint = '/fetch/diff/files'; + const mock = new MockAdapter(axios); + const res = { diff_files: 1, merge_request_diffs: [] }; + mock.onGet(endpoint).reply(200, res); + + testAction( + actions.fetchDiffFiles, + {}, + { endpoint }, + [ + { type: types.SET_LOADING, payload: true }, + { type: types.SET_LOADING, payload: false }, + { type: types.SET_MERGE_REQUEST_DIFFS, payload: res.merge_request_diffs }, + { type: types.SET_DIFF_DATA, payload: res }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('setInlineDiffViewType', () => { + it('should set diff view type to inline and also set the cookie properly', done => { + testAction( + actions.setInlineDiffViewType, + null, + {}, + [{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }], + [], + () => { + setTimeout(() => { + expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE); + done(); + }, 0); + }, + ); + }); + }); + + describe('setParallelDiffViewType', () => { + it('should set diff view type to parallel and also set the cookie properly', done => { + testAction( + actions.setParallelDiffViewType, + null, + {}, + [{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }], + [], + () => { + setTimeout(() => { + expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE); + done(); + }, 0); + }, + ); + }); + }); + + describe('showCommentForm', () => { + it('should call mutation to show comment form', done => { + const payload = { lineCode: 'lineCode' }; + + testAction( + actions.showCommentForm, + payload, + {}, + [{ type: types.ADD_COMMENT_FORM_LINE, payload }], + [], + done, + ); + }); + }); + + describe('cancelCommentForm', () => { + it('should call mutation to cancel comment form', done => { + const payload = { lineCode: 'lineCode' }; + + testAction( + actions.cancelCommentForm, + payload, + {}, + [{ type: types.REMOVE_COMMENT_FORM_LINE, payload }], + [], + done, + ); + }); + }); + + describe('loadMoreLines', () => { + it('should call mutation to show comment form', done => { + const endpoint = '/diffs/load/more/lines'; + const params = { since: 6, to: 26 }; + const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; + const fileHash = 'ff9200'; + const options = { endpoint, params, lineNumbers, fileHash }; + const mock = new MockAdapter(axios); + const contextLines = { contextLines: [{ lineCode: 6 }] }; + mock.onGet(endpoint).reply(200, contextLines); + + testAction( + actions.loadMoreLines, + options, + {}, + [ + { + type: types.ADD_CONTEXT_LINES, + payload: { lineNumbers, contextLines, params, fileHash }, + }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('loadCollapsedDiff', () => { + it('should fetch data and call mutation with response and the give parameter', done => { + const file = { hash: 123, loadCollapsedDiffUrl: '/load/collapsed/diff/url' }; + const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] }; + const mock = new MockAdapter(axios); + mock.onGet(file.loadCollapsedDiffUrl).reply(200, data); + + testAction( + actions.loadCollapsedDiff, + file, + {}, + [ + { + type: types.ADD_COLLAPSED_DIFFS, + payload: { file, data }, + }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('expandAllFiles', () => { + it('should change the collapsed prop from the diffFiles', done => { + testAction( + actions.expandAllFiles, + null, + {}, + [ + { + type: types.EXPAND_ALL_FILES, + }, + ], + [], + done, + ); + }); + }); +}); diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js new file mode 100644 index 00000000000..7945ddea911 --- /dev/null +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -0,0 +1,24 @@ +import getters from '~/diffs/store/getters'; +import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; + +describe('DiffsStoreGetters', () => { + describe('isParallelView', () => { + it('should return true if view set to parallel view', () => { + expect(getters.isParallelView({ diffViewType: PARALLEL_DIFF_VIEW_TYPE })).toBeTruthy(); + }); + + it('should return false if view not to parallel view', () => { + expect(getters.isParallelView({ diffViewType: 'foo' })).toBeFalsy(); + }); + }); + + describe('isInlineView', () => { + it('should return true if view set to inline view', () => { + expect(getters.isInlineView({ diffViewType: INLINE_DIFF_VIEW_TYPE })).toBeTruthy(); + }); + + it('should return false if view not to inline view', () => { + expect(getters.isInlineView({ diffViewType: PARALLEL_DIFF_VIEW_TYPE })).toBeFalsy(); + }); + }); +}); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js new file mode 100644 index 00000000000..5f1a6e9def7 --- /dev/null +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -0,0 +1,147 @@ +import mutations from '~/diffs/store/mutations'; +import * as types from '~/diffs/store/mutation_types'; +import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; + +describe('DiffsStoreMutations', () => { + describe('SET_ENDPOINT', () => { + it('should set endpoint', () => { + const state = {}; + const endpoint = '/diffs/endpoint'; + + mutations[types.SET_ENDPOINT](state, endpoint); + expect(state.endpoint).toEqual(endpoint); + }); + }); + + describe('SET_LOADING', () => { + it('should set loading state', () => { + const state = {}; + + mutations[types.SET_LOADING](state, false); + expect(state.isLoading).toEqual(false); + }); + }); + + describe('SET_DIFF_FILES', () => { + it('should set diff files to state', () => { + const filePath = '/first-diff-file-path'; + const state = {}; + const diffFiles = { + a_mode: 1, + highlighted_diff_lines: [{ file_path: filePath }], + }; + + mutations[types.SET_DIFF_FILES](state, diffFiles); + expect(state.diffFiles.aMode).toEqual(1); + expect(state.diffFiles.highlightedDiffLines[0].filePath).toEqual(filePath); + }); + }); + + describe('SET_DIFF_VIEW_TYPE', () => { + it('should set diff view type properly', () => { + const state = {}; + + mutations[types.SET_DIFF_VIEW_TYPE](state, INLINE_DIFF_VIEW_TYPE); + expect(state.diffViewType).toEqual(INLINE_DIFF_VIEW_TYPE); + }); + }); + + describe('ADD_COMMENT_FORM_LINE', () => { + it('should set a truthy reference for the given line code in diffLineCommentForms', () => { + const state = { diffLineCommentForms: {} }; + const lineCode = 'FDE'; + + mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode }); + expect(state.diffLineCommentForms[lineCode]).toBeTruthy(); + }); + }); + + describe('REMOVE_COMMENT_FORM_LINE', () => { + it('should remove given reference from diffLineCommentForms', () => { + const state = { diffLineCommentForms: {} }; + const lineCode = 'FDE'; + + mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode }); + expect(state.diffLineCommentForms[lineCode]).toBeTruthy(); + + mutations[types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }); + expect(state.diffLineCommentForms[lineCode]).toBeUndefined(); + }); + }); + + describe('EXPAND_ALL_FILES', () => { + it('should change the collapsed prop from diffFiles', () => { + const diffFile = { + collapsed: true, + }; + const state = { expandAllFiles: true, diffFiles: [diffFile] }; + + mutations[types.EXPAND_ALL_FILES](state); + expect(state.diffFiles[0].collapsed).toEqual(false); + }); + }); + + describe('ADD_CONTEXT_LINES', () => { + it('should call utils.addContextLines with proper params', () => { + const options = { + lineNumbers: { oldLineNumber: 1, newLineNumber: 2 }, + contextLines: [{ oldLine: 1 }], + fileHash: 'ff9200', + params: { + bottom: true, + }, + }; + const diffFile = { + fileHash: options.fileHash, + highlightedDiffLines: [], + parallelDiffLines: [], + }; + const state = { diffFiles: [diffFile] }; + const lines = [{ oldLine: 1 }]; + + const findDiffFileSpy = spyOnDependency(mutations, 'findDiffFile').and.returnValue(diffFile); + const removeMatchLineSpy = spyOnDependency(mutations, 'removeMatchLine'); + const lineRefSpy = spyOnDependency(mutations, 'addLineReferences').and.returnValue(lines); + const addContextLinesSpy = spyOnDependency(mutations, 'addContextLines'); + + mutations[types.ADD_CONTEXT_LINES](state, options); + + expect(findDiffFileSpy).toHaveBeenCalledWith(state.diffFiles, options.fileHash); + expect(removeMatchLineSpy).toHaveBeenCalledWith( + diffFile, + options.lineNumbers, + options.params.bottom, + ); + expect(lineRefSpy).toHaveBeenCalledWith( + options.contextLines, + options.lineNumbers, + options.params.bottom, + ); + expect(addContextLinesSpy).toHaveBeenCalledWith({ + inlineLines: diffFile.highlightedDiffLines, + parallelLines: diffFile.parallelDiffLines, + contextLines: options.contextLines, + bottom: options.params.bottom, + lineNumbers: options.lineNumbers, + }); + }); + }); + + describe('ADD_COLLAPSED_DIFFS', () => { + it('should update the state with the given data for the given file hash', () => { + const spy = spyOnDependency(mutations, 'convertObjectPropsToCamelCase').and.callThrough(); + + const fileHash = 123; + const state = { diffFiles: [{}, { fileHash, existingField: 0 }] }; + const file = { fileHash }; + const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existingField: 1 }] }; + + mutations[types.ADD_COLLAPSED_DIFFS](state, { file, data }); + expect(spy).toHaveBeenCalledWith(data, { deep: true }); + + expect(state.diffFiles[1].fileHash).toEqual(fileHash); + expect(state.diffFiles[1].existingField).toEqual(1); + expect(state.diffFiles[1].extraField).toEqual(1); + }); + }); +}); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js new file mode 100644 index 00000000000..5a024a0f2ad --- /dev/null +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -0,0 +1,179 @@ +import * as utils from '~/diffs/store/utils'; +import { + LINE_POSITION_LEFT, + LINE_POSITION_RIGHT, + TEXT_DIFF_POSITION_TYPE, + DIFF_NOTE_TYPE, + NEW_LINE_TYPE, + OLD_LINE_TYPE, + MATCH_LINE_TYPE, + PARALLEL_DIFF_VIEW_TYPE, +} from '~/diffs/constants'; +import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants'; +import diffFileMockData from '../mock_data/diff_file'; +import { noteableDataMock } from '../../notes/mock_data'; + +const getDiffFileMock = () => Object.assign({}, diffFileMockData); + +describe('DiffsStoreUtils', () => { + describe('findDiffFile', () => { + const files = [{ fileHash: 1, name: 'one' }]; + + it('should return correct file', () => { + expect(utils.findDiffFile(files, 1).name).toEqual('one'); + expect(utils.findDiffFile(files, 2)).toBeUndefined(); + }); + }); + + describe('getReversePosition', () => { + it('should return correct line position name', () => { + expect(utils.getReversePosition(LINE_POSITION_RIGHT)).toEqual(LINE_POSITION_LEFT); + expect(utils.getReversePosition(LINE_POSITION_LEFT)).toEqual(LINE_POSITION_RIGHT); + }); + }); + + describe('findIndexInInlineLines and findIndexInParallelLines', () => { + const expectSet = (method, lines, invalidLines) => { + expect(method(lines, { oldLineNumber: 3, newLineNumber: 5 })).toEqual(4); + expect(method(invalidLines || lines, { oldLineNumber: 32, newLineNumber: 53 })).toEqual(-1); + }; + + describe('findIndexInInlineLines', () => { + it('should return correct index for given line numbers', () => { + expectSet(utils.findIndexInInlineLines, getDiffFileMock().highlightedDiffLines); + }); + }); + + describe('findIndexInParallelLines', () => { + it('should return correct index for given line numbers', () => { + expectSet(utils.findIndexInParallelLines, getDiffFileMock().parallelDiffLines, {}); + }); + }); + }); + + describe('removeMatchLine', () => { + it('should remove match line properly by regarding the bottom parameter', () => { + const diffFile = getDiffFileMock(); + const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; + const inlineIndex = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers); + const parallelIndex = utils.findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers); + const atInlineIndex = diffFile.highlightedDiffLines[inlineIndex]; + const atParallelIndex = diffFile.parallelDiffLines[parallelIndex]; + + utils.removeMatchLine(diffFile, lineNumbers, false); + expect(diffFile.highlightedDiffLines[inlineIndex]).not.toEqual(atInlineIndex); + expect(diffFile.parallelDiffLines[parallelIndex]).not.toEqual(atParallelIndex); + + utils.removeMatchLine(diffFile, lineNumbers, true); + expect(diffFile.highlightedDiffLines[inlineIndex + 1]).not.toEqual(atInlineIndex); + expect(diffFile.parallelDiffLines[parallelIndex + 1]).not.toEqual(atParallelIndex); + }); + }); + + describe('addContextLines', () => { + it('should add context lines properly with bottom parameter', () => { + const diffFile = getDiffFileMock(); + const inlineLines = diffFile.highlightedDiffLines; + const parallelLines = diffFile.parallelDiffLines; + const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; + const contextLines = [{ lineNumber: 42 }]; + const options = { inlineLines, parallelLines, contextLines, lineNumbers, bottom: true }; + const inlineIndex = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers); + const parallelIndex = utils.findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers); + const normalizedParallelLine = { + left: options.contextLines[0], + right: options.contextLines[0], + }; + + utils.addContextLines(options); + expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]); + expect(parallelLines[parallelLines.length - 1]).toEqual(normalizedParallelLine); + + delete options.bottom; + utils.addContextLines(options); + expect(inlineLines[inlineIndex]).toEqual(contextLines[0]); + expect(parallelLines[parallelIndex]).toEqual(normalizedParallelLine); + }); + }); + + describe('getNoteFormData', () => { + it('should properly create note form data', () => { + const diffFile = getDiffFileMock(); + noteableDataMock.targetType = MERGE_REQUEST_NOTEABLE_TYPE; + + const options = { + note: 'Hello world!', + noteableData: noteableDataMock, + noteableType: MERGE_REQUEST_NOTEABLE_TYPE, + diffFile, + noteTargetLine: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + metaData: null, + newLine: 3, + oldLine: 1, + }, + diffViewType: PARALLEL_DIFF_VIEW_TYPE, + linePosition: LINE_POSITION_LEFT, + }; + + const position = JSON.stringify({ + base_sha: diffFile.diffRefs.baseSha, + start_sha: diffFile.diffRefs.startSha, + head_sha: diffFile.diffRefs.headSha, + old_path: diffFile.oldPath, + new_path: diffFile.newPath, + position_type: TEXT_DIFF_POSITION_TYPE, + old_line: options.noteTargetLine.oldLine, + new_line: options.noteTargetLine.newLine, + }); + + const postData = { + view: options.diffViewType, + line_type: options.linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE, + merge_request_diff_head_sha: diffFile.diffRefs.headSha, + in_reply_to_discussion_id: '', + note_project_id: '', + target_type: options.noteableType, + target_id: options.noteableData.id, + note: { + noteable_type: options.noteableType, + noteable_id: options.noteableData.id, + commit_id: '', + type: DIFF_NOTE_TYPE, + line_code: options.noteTargetLine.lineCode, + note: options.note, + position, + }, + }; + + expect(utils.getNoteFormData(options)).toEqual({ + endpoint: options.noteableData.create_note_path, + data: postData, + }); + }); + }); + + describe('addLineReferences', () => { + const lineNumbers = { oldLineNumber: 3, newLineNumber: 4 }; + + it('should add correct line references when bottom set to true', () => { + const lines = [{ type: null }, { type: MATCH_LINE_TYPE }]; + const linesWithReferences = utils.addLineReferences(lines, lineNumbers, true); + + expect(linesWithReferences[0].oldLine).toEqual(lineNumbers.oldLineNumber + 1); + expect(linesWithReferences[0].newLine).toEqual(lineNumbers.newLineNumber + 1); + expect(linesWithReferences[1].metaData.oldPos).toEqual(4); + expect(linesWithReferences[1].metaData.newPos).toEqual(5); + }); + + it('should add correct line references when bottom falsy', () => { + const lines = [{ type: null }, { type: MATCH_LINE_TYPE }, { type: null }]; + const linesWithReferences = utils.addLineReferences(lines, lineNumbers); + + expect(linesWithReferences[0].oldLine).toEqual(0); + expect(linesWithReferences[0].newLine).toEqual(1); + expect(linesWithReferences[1].metaData.oldPos).toEqual(2); + expect(linesWithReferences[1].metaData.newPos).toEqual(3); + }); + }); +}); diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb new file mode 100644 index 00000000000..351db6ba184 --- /dev/null +++ b/spec/javascripts/fixtures/commit.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + let(:commit) { project.commit("master") } + + render_views + + before(:all) do + clean_frontend_fixtures('commit/') + end + + before do + project.add_master(user) + sign_in(user) + end + + it 'commit/show.html.raw' do |example| + params = { + namespace_id: project.namespace, + project_id: project, + id: commit.id + } + + get :show, params + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb index fa97f352e31..38fc963caf7 100644 --- a/spec/javascripts/fixtures/snippet.rb +++ b/spec/javascripts/fixtures/snippet.rb @@ -7,6 +7,7 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } let(:snippet) { create(:personal_snippet, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: admin) } + let!(:snippet_note) { create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') } render_views diff --git a/spec/javascripts/helpers/index.js b/spec/javascripts/helpers/index.js new file mode 100644 index 00000000000..d2c5caf0bdb --- /dev/null +++ b/spec/javascripts/helpers/index.js @@ -0,0 +1,3 @@ +import mountComponent, { mountComponentWithStore } from './vue_mount_component_helper'; + +export { mountComponent, mountComponentWithStore }; diff --git a/spec/javascripts/helpers/init_vue_mr_page_helper.js b/spec/javascripts/helpers/init_vue_mr_page_helper.js new file mode 100644 index 00000000000..921d42a0871 --- /dev/null +++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js @@ -0,0 +1,40 @@ +import initMRPage from '~/mr_notes/index'; +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; +import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data'; +import diffFileMockData from '../diffs/mock_data/diff_file'; + +export default function initVueMRPage() { + const diffsAppEndpoint = '/diffs/app/endpoint'; + const mrEl = document.createElement('div'); + mrEl.className = 'merge-request fixture-mr'; + mrEl.setAttribute('data-mr-action', 'diffs'); + document.body.appendChild(mrEl); + + const mrDiscussionsEl = document.createElement('div'); + mrDiscussionsEl.id = 'js-vue-mr-discussions'; + mrDiscussionsEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock)); + mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock)); + mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock)); + mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request'); + document.body.appendChild(mrDiscussionsEl); + + const discussionCounterEl = document.createElement('div'); + discussionCounterEl.id = 'js-vue-discussion-counter'; + document.body.appendChild(discussionCounterEl); + + const diffsAppEl = document.createElement('div'); + diffsAppEl.id = 'js-diffs-app'; + diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint); + diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock)); + document.body.appendChild(diffsAppEl); + + const mock = new MockAdapter(axios); + mock.onGet(diffsAppEndpoint).reply(200, { + branch_name: 'foo', + diff_files: [diffFileMockData], + }); + + initMRPage(); + return mock; +} diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index a9ec7f42a9d..41ff59949e5 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -556,5 +556,75 @@ describe('common_utils', () => { expect(Object.keys(commonUtils.convertObjectPropsToCamelCase()).length).toBe(0); expect(Object.keys(commonUtils.convertObjectPropsToCamelCase({})).length).toBe(0); }); + + it('does not deep-convert by default', () => { + const obj = { + snake_key: { + child_snake_key: 'value', + }, + }; + + expect( + commonUtils.convertObjectPropsToCamelCase(obj), + ).toEqual({ + snakeKey: { + child_snake_key: 'value', + }, + }); + }); + + describe('deep: true', () => { + it('converts object with child objects', () => { + const obj = { + snake_key: { + child_snake_key: 'value', + }, + }; + + expect( + commonUtils.convertObjectPropsToCamelCase(obj, { deep: true }), + ).toEqual({ + snakeKey: { + childSnakeKey: 'value', + }, + }); + }); + + it('converts array with child objects', () => { + const arr = [ + { + child_snake_key: 'value', + }, + ]; + + expect( + commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }), + ).toEqual([ + { + childSnakeKey: 'value', + }, + ]); + }); + + it('converts array with child arrays', () => { + const arr = [ + [ + { + child_snake_key: 'value', + }, + ], + ]; + + expect( + commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }), + ).toEqual([ + [ + { + childSnakeKey: 'value', + }, + ], + ]); + }); + }); }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index eab5c24406a..33987574f00 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -96,4 +96,20 @@ describe('text_utility', () => { expect(textUtils.convertToSentenceCase('Hello World')).toBe('Hello world'); }); }); + + describe('truncateSha', () => { + it('shortens SHAs to 8 characters', () => { + expect(textUtils.truncateSha('verylongsha')).toBe('verylong'); + }); + + it('leaves short SHAs as is', () => { + expect(textUtils.truncateSha('shortsha')).toBe('shortsha'); + }); + }); + + describe('splitCamelCase', () => { + it('separates a PascalCase word to two', () => { + expect(textUtils.splitCamelCase('HelloWorld')).toBe('Hello World'); + }); + }); }); diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js index 7cc5e753c22..0d465510fd3 100644 --- a/spec/javascripts/matchers.js +++ b/spec/javascripts/matchers.js @@ -1,4 +1,16 @@ export default { + toContainText: () => ({ + compare(vm, text) { + if (!(vm.$el instanceof HTMLElement)) { + throw new Error('vm.$el is not a DOM element!'); + } + + const result = { + pass: vm.$el.innerText.includes(text), + }; + return result; + }, + }), toHaveSpriteIcon: () => ({ compare(element, iconName) { if (!iconName) { @@ -10,7 +22,9 @@ export default { } const iconReferences = [].slice.apply(element.querySelectorAll('svg use')); - const matchingIcon = iconReferences.find(reference => reference.getAttribute('xlink:href').endsWith(`#${iconName}`)); + const matchingIcon = iconReferences.find(reference => + reference.getAttribute('xlink:href').endsWith(`#${iconName}`), + ); const result = { pass: !!matchingIcon, }; @@ -20,7 +34,7 @@ export default { } else { result.message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`; - const existingIcons = iconReferences.map((reference) => { + const existingIcons = iconReferences.map(reference => { const iconUrl = reference.getAttribute('xlink:href'); return `"${iconUrl.replace(/^.+#/, '')}"`; }); @@ -32,4 +46,12 @@ export default { return result; }, }), + toRender: () => ({ + compare(vm) { + const result = { + pass: vm.$el.nodeType !== Node.COMMENT_NODE, + }; + return result; + }, + }), }; diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js deleted file mode 100644 index dc9dc4d4249..00000000000 --- a/spec/javascripts/merge_request_notes_spec.js +++ /dev/null @@ -1,108 +0,0 @@ -import $ from 'jquery'; -import _ from 'underscore'; -import 'autosize'; -import '~/gl_form'; -import '~/lib/utils/text_utility'; -import '~/behaviors/markdown/render_gfm'; -import Notes from '~/notes'; - -const upArrowKeyCode = 38; - -describe('Merge request notes', () => { - window.gon = window.gon || {}; - window.gl = window.gl || {}; - gl.utils = gl.utils || {}; - - const discussionTabFixture = 'merge_requests/diff_comment.html.raw'; - const changesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json'; - preloadFixtures(discussionTabFixture, changesTabJsonFixture); - - describe('Discussion tab with diff comments', () => { - beforeEach(() => { - loadFixtures(discussionTabFixture); - gl.utils.disableButtonIfEmptyField = _.noop; - window.project_uploads_path = 'http://test.host/uploads'; - $('body').attr('data-page', 'projects:merge_requests:show'); - window.gon.current_user_id = $('.note:last').data('authorId'); - - return new Notes('', []); - }); - - afterEach(() => { - // Undo what we did to the shared <body> - $('body').removeAttr('data-page'); - }); - - describe('up arrow', () => { - it('edits last comment when triggered in main form', () => { - const upArrowEvent = $.Event('keydown'); - upArrowEvent.which = upArrowKeyCode; - - spyOnEvent('.note:last .js-note-edit', 'click'); - - $('.js-note-text').trigger(upArrowEvent); - - expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); - }); - - it('edits last comment in discussion when triggered in discussion form', (done) => { - const upArrowEvent = $.Event('keydown'); - upArrowEvent.which = upArrowKeyCode; - - spyOnEvent('.note-discussion .js-note-edit', 'click'); - - $('.js-discussion-reply-button').click(); - - setTimeout(() => { - expect( - $('.note-discussion .js-note-text'), - ).toExist(); - - $('.note-discussion .js-note-text').trigger(upArrowEvent); - - expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit'); - - done(); - }); - }); - }); - }); - - describe('Changes tab with diff comments', () => { - beforeEach(() => { - const diffsResponse = getJSONFixture(changesTabJsonFixture); - const noteFormHtml = `<form class="js-new-note-form"> - <textarea class="js-note-text"></textarea> - </form>`; - setFixtures(diffsResponse.html + noteFormHtml); - $('body').attr('data-page', 'projects:merge_requests:show'); - window.gon.current_user_id = $('.note:last').data('authorId'); - - return new Notes('', []); - }); - - afterEach(() => { - // Undo what we did to the shared <body> - $('body').removeAttr('data-page'); - }); - - describe('up arrow', () => { - it('edits last comment in discussion when triggered in discussion form', (done) => { - const upArrowEvent = $.Event('keydown'); - upArrowEvent.which = upArrowKeyCode; - - spyOnEvent('.note:last .js-note-edit', 'click'); - - $('.js-discussion-reply-button').trigger('click'); - - setTimeout(() => { - $('.js-note-text').trigger(upArrowEvent); - - expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); - - done(); - }); - }); - }); - }); -}); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 3dbd9756cd2..08928e13985 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var, comma-dangle, object-shorthand */ - +/* eslint-disable no-var, object-shorthand */ import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; @@ -7,480 +6,228 @@ import MergeRequestTabs from '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; import '~/breakpoints'; import '~/lib/utils/common_utils'; -import Diff from '~/diff'; -import Notes from '~/notes'; import 'vendor/jquery.scrollTo'; - -(function () { - describe('MergeRequestTabs', function () { - var stubLocation = {}; - var setLocation = function (stubs) { - var defaults = { - pathname: '', - search: '', - hash: '' - }; - $.extend(stubLocation, defaults, stubs || {}); +import initMrPage from './helpers/init_vue_mr_page_helper'; + +describe('MergeRequestTabs', function() { + let mrPageMock; + var stubLocation = {}; + var setLocation = function(stubs) { + var defaults = { + pathname: '', + search: '', + hash: '', }; + $.extend(stubLocation, defaults, stubs || {}); + }; - const inlineChangesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json'; - const parallelChangesTabJsonFixture = 'merge_request_diffs/parallel_changes_tab_with_comments.json'; - preloadFixtures( - 'merge_requests/merge_request_with_task_list.html.raw', - 'merge_requests/diff_comment.html.raw', - inlineChangesTabJsonFixture, - parallelChangesTabJsonFixture - ); - - beforeEach(function () { - this.class = new MergeRequestTabs({ stubLocation: stubLocation }); - setLocation(); - - this.spies = { - history: spyOn(window.history, 'replaceState').and.callFake(function () {}) - }; - }); - - afterEach(function () { - this.class.unbindEvents(); - this.class.destroyPipelinesView(); - }); - - describe('activateTab', function () { - beforeEach(function () { - spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - this.subject = this.class.activateTab; - }); - it('shows the notes tab when action is show', function () { - this.subject('show'); - expect($('#notes')).toHaveClass('active'); - }); - it('shows the commits tab when action is commits', function () { - this.subject('commits'); - expect($('#commits')).toHaveClass('active'); - }); - it('shows the diffs tab when action is diffs', function () { - this.subject('diffs'); - expect($('#diffs')).toHaveClass('active'); - }); - }); - - describe('opensInNewTab', function () { - var tabUrl; - var windowTarget = '_blank'; - - beforeEach(function () { - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - - tabUrl = $('.commits-tab a').attr('href'); + preloadFixtures( + 'merge_requests/merge_request_with_task_list.html.raw', + 'merge_requests/diff_comment.html.raw', + ); - spyOn($.fn, 'attr').and.returnValue(tabUrl); - }); - - describe('meta click', () => { - let metakeyEvent; - beforeEach(function () { - metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); - }); + beforeEach(function() { + mrPageMock = initMrPage(); + this.class = new MergeRequestTabs({ stubLocation: stubLocation }); + setLocation(); - it('opens page when commits link is clicked', function () { - spyOn(window, 'open').and.callFake(function (url, name) { - expect(url).toEqual(tabUrl); - expect(name).toEqual(windowTarget); - }); + this.spies = { + history: spyOn(window.history, 'replaceState').and.callFake(function() {}), + }; + }); - this.class.bindEvents(); - $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent); - }); + afterEach(function() { + this.class.unbindEvents(); + this.class.destroyPipelinesView(); + mrPageMock.restore(); + }); - it('opens page when commits badge is clicked', function () { - spyOn(window, 'open').and.callFake(function (url, name) { - expect(url).toEqual(tabUrl); - expect(name).toEqual(windowTarget); - }); + describe('opensInNewTab', function() { + var tabUrl; + var windowTarget = '_blank'; - this.class.bindEvents(); - $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent); - }); - }); + beforeEach(function() { + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () { - spyOn(window, 'open').and.callFake(function (url, name) { - expect(url).toEqual(tabUrl); - expect(name).toEqual(windowTarget); - }); + tabUrl = $('.commits-tab a').attr('href'); + }); - this.class.clickTab({ - metaKey: false, - ctrlKey: true, - which: 1, - stopImmediatePropagation: function () {} - }); + describe('meta click', () => { + let metakeyEvent; + beforeEach(function() { + metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); }); - 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 when commits link is clicked', function() { + spyOn(window, 'open').and.callFake(function(url, name) { expect(url).toEqual(tabUrl); expect(name).toEqual(windowTarget); }); - this.class.clickTab({ - metaKey: true, - ctrlKey: false, - which: 1, - stopImmediatePropagation: function () {} - }); + this.class.bindEvents(); + $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent); }); - 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 when commits badge is clicked', function() { + spyOn(window, 'open').and.callFake(function(url, name) { expect(url).toEqual(tabUrl); expect(name).toEqual(windowTarget); }); - this.class.clickTab({ - metaKey: false, - ctrlKey: false, - which: 2, - stopImmediatePropagation: function () {} - }); + this.class.bindEvents(); + $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent); }); }); - describe('setCurrentAction', function () { - beforeEach(function () { - spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); - this.subject = this.class.setCurrentAction; - }); - - it('changes from commits', function () { - 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'); - }); - - it('changes from diffs', function () { - 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'); + it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function() { + spyOn(window, 'open').and.callFake(function(url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); }); - it('changes from diffs.html', function () { - 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'); + this.class.clickTab({ + metaKey: false, + ctrlKey: true, + which: 1, + stopImmediatePropagation: function() {}, }); + }); - it('changes from notes', function () { - 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'); + it('opens page tab in a new browser tab with Cmd+Click - Mac', function() { + spyOn(window, 'open').and.callFake(function(url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); }); - it('includes search parameters and hash string', function () { - 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'); + this.class.clickTab({ + metaKey: true, + ctrlKey: false, + which: 1, + stopImmediatePropagation: function() {}, }); + }); - it('replaces the current history state', function () { - var newState; - setLocation({ - pathname: '/foo/bar/merge_requests/1' - }); - newState = this.subject('commits'); - expect(this.spies.history).toHaveBeenCalledWith({ - url: newState - }, document.title, newState); + it('opens page tab in a new browser tab with Middle-click - Mac/PC', function() { + spyOn(window, 'open').and.callFake(function(url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual(windowTarget); }); - it('treats "show" like "notes"', function () { - setLocation({ - pathname: '/foo/bar/merge_requests/1/commits' - }); - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + this.class.clickTab({ + metaKey: false, + ctrlKey: false, + which: 2, + stopImmediatePropagation: function() {}, }); }); + }); - describe('tabShown', () => { - let mock; + describe('setCurrentAction', function() { + let mock; - beforeEach(function () { - mock = new MockAdapter(axios); - mock.onGet(/(.*)\/diffs\.json/).reply(200, { - data: { html: '' }, - }); + beforeEach(function() { + mock = new MockAdapter(axios); + mock.onAny().reply({ data: {} }); + this.subject = this.class.setCurrentAction; + }); - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); - }); + afterEach(() => { + mock.restore(); + }); - afterEach(() => { - mock.restore(); + it('changes from commits', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1/commits', }); - describe('with "Side-by-side"/parallel diff view', () => { - beforeEach(function () { - this.class.diffViewType = () => 'parallel'; - Diff.prototype.diffViewType = () => 'parallel'; - }); - - it('maintains `container-limited` for pipelines tab', function (done) { - const asyncClick = function (selector) { - return new Promise((resolve) => { - setTimeout(() => { - document.querySelector(selector).click(); - resolve(); - }); - }); - }; - asyncClick('.merge-request-tabs .pipelines-tab a') - .then(() => asyncClick('.merge-request-tabs .diffs-tab a')) - .then(() => asyncClick('.merge-request-tabs .pipelines-tab a')) - .then(() => { - const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited'); - expect(hasContainerLimitedClass).toBe(true); - }) - .then(done) - .catch((err) => { - done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`); - }); - }); - - it('maintains `container-limited` when switching from "Changes" tab before it loads', function (done) { - const asyncClick = function (selector) { - return new Promise((resolve) => { - setTimeout(() => { - document.querySelector(selector).click(); - resolve(); - }); - }); - }; - - asyncClick('.merge-request-tabs .diffs-tab a') - .then(() => asyncClick('.merge-request-tabs .notes-tab a')) - .then(() => { - const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited'); - expect(hasContainerLimitedClass).toBe(true); - }) - .then(done) - .catch((err) => { - done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`); - }); - }); - }); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); }); - describe('loadDiff', function () { - beforeEach(() => { - loadFixtures('merge_requests/diff_comment.html.raw'); - $('body').attr('data-page', 'projects:merge_requests:show'); - window.gl.ImageFile = () => {}; - Notes.initialize('', []); - spyOn(Notes.instance, 'toggleDiffNote').and.callThrough(); + it('changes from diffs', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1/diffs', }); - afterEach(() => { - delete window.gl.ImageFile; - delete window.notes; + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); - // Undo what we did to the shared <body> - $('body').removeAttr('data-page'); + it('changes from diffs.html', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1/diffs.html', }); - it('triggers Ajax request to JSON endpoint', function (done) { - const url = '/foo/bar/merge_requests/1/diffs'; - - spyOn(axios, 'get').and.callFake((reqUrl) => { - expect(reqUrl).toBe(`${url}.json`); - - done(); - - return Promise.resolve({ data: {} }); - }); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); - this.class.loadDiff(url); + it('changes from notes', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1', }); - it('triggers scroll event when diff already loaded', function (done) { - spyOn(axios, 'get').and.callFake(done.fail); - spyOn(document, 'dispatchEvent'); - - this.class.diffsLoaded = true; - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); + expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); - expect( - document.dispatchEvent, - ).toHaveBeenCalledWith(new CustomEvent('scroll')); - done(); + it('includes search parameters and hash string', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1/diffs', + search: '?view=parallel', + hash: '#L15-35', }); - describe('with inline diff', () => { - let noteId; - let noteLineNumId; - let mock; - - beforeEach(() => { - const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture); - - const $html = $(diffsResponse.html); - noteId = $html.find('.note').attr('id'); - noteLineNumId = $html - .find('.note') - .closest('.notes_holder') - .prev('.line_holder') - .find('a[data-linenumber]') - .attr('href') - .replace('#', ''); - - mock = new MockAdapter(axios); - mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('with note fragment hash', () => { - it('should expand and scroll to linked fragment hash #note_xxx', function (done) { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - - setTimeout(() => { - expect(noteId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ - target: jasmine.any(Object), - lineType: 'old', - forceShow: true, - }); - - done(); - }); - }); - - it('should gracefully ignore non-existant fragment hash', function (done) { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - - setTimeout(() => { - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); - - done(); - }); - }); - }); - - describe('with line number fragment hash', () => { - it('should gracefully ignore line number fragment hash', function () { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); + }); - expect(noteLineNumId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); - }); - }); + it('replaces the current history state', function() { + var newState; + setLocation({ + pathname: '/foo/bar/merge_requests/1', }); + newState = this.subject('commits'); - describe('with parallel diff', () => { - let noteId; - let noteLineNumId; - let mock; - - beforeEach(() => { - const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture); - - const $html = $(diffsResponse.html); - noteId = $html.find('.note').attr('id'); - noteLineNumId = $html - .find('.note') - .closest('.notes_holder') - .prev('.line_holder') - .find('a[data-linenumber]') - .attr('href') - .replace('#', ''); - - mock = new MockAdapter(axios); - mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('with note fragment hash', () => { - it('should expand and scroll to linked fragment hash #note_xxx', function (done) { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId); - - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - - setTimeout(() => { - expect(noteId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ - target: jasmine.any(Object), - lineType: 'new', - forceShow: true, - }); - - done(); - }); - }); - - it('should gracefully ignore non-existant fragment hash', function (done) { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - - setTimeout(() => { - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); - done(); - }); - }); - }); - - describe('with line number fragment hash', () => { - it('should gracefully ignore line number fragment hash', function () { - spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + expect(this.spies.history).toHaveBeenCalledWith( + { + url: newState, + }, + document.title, + newState, + ); + }); - expect(noteLineNumId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); - }); - }); + it('treats "show" like "notes"', function() { + setLocation({ + pathname: '/foo/bar/merge_requests/1/commits', }); + + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); }); + }); - describe('expandViewContainer', function () { - beforeEach(() => { - $('body').append('<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>'); - }); + describe('expandViewContainer', function() { + beforeEach(() => { + $('body').append( + '<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>', + ); + }); - afterEach(() => { - $('.content-wrapper').remove(); - }); + afterEach(() => { + $('.content-wrapper').remove(); + }); - it('removes container-limited from containers', function () { - this.class.expandViewContainer(); + it('removes container-limited from containers', function() { + this.class.expandViewContainer(); - expect($('.content-wrapper')).not.toContainElement('.container-limited'); - }); + expect($('.content-wrapper')).not.toContainElement('.container-limited'); + }); - it('does remove container-limited from breadcrumbs', function () { - $('.container-limited').addClass('breadcrumbs'); - this.class.expandViewContainer(); + it('does remove container-limited from breadcrumbs', function() { + $('.container-limited').addClass('breadcrumbs'); + this.class.expandViewContainer(); - expect($('.content-wrapper')).toContainElement('.container-limited'); - }); + expect($('.content-wrapper')).toContainElement('.container-limited'); }); }); -}).call(window); +}); diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index a7d1e4331eb..155c91dcc46 100644 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -1,23 +1,27 @@ import $ from 'jquery'; import Vue from 'vue'; import Autosize from 'autosize'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import CommentForm from '~/notes/components/comment_form.vue'; +import * as constants from '~/notes/constants'; import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data'; import { keyboardDownEvent } from '../../issue_show/helpers'; describe('issue_comment_form component', () => { + let store; let vm; const Component = Vue.extend(CommentForm); let mountComponent; beforeEach(() => { - mountComponent = (noteableType = 'issue') => new Component({ - propsData: { - noteableType, - }, - store, - }).$mount(); + store = createStore(); + mountComponent = (noteableType = 'issue') => + new Component({ + propsData: { + noteableType, + }, + store, + }).$mount(); }); afterEach(() => { @@ -34,7 +38,9 @@ describe('issue_comment_form component', () => { }); it('should render user avatar with link', () => { - expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); + expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual( + userDataMock.path, + ); }); describe('handleSave', () => { @@ -60,7 +66,7 @@ describe('issue_comment_form component', () => { expect(vm.toggleIssueState).toHaveBeenCalled(); }); - it('should disable action button whilst submitting', (done) => { + it('should disable action button whilst submitting', done => { const saveNotePromise = Promise.resolve(); vm.note = 'hello world'; spyOn(vm, 'saveNote').and.returnValue(saveNotePromise); @@ -87,16 +93,18 @@ describe('issue_comment_form component', () => { ).toEqual('Write a comment or drag your files here…'); }); - it('should make textarea disabled while requesting', (done) => { + 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. + 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. + vm.$nextTick(() => { + // Wait for vm.isSubmitting triggered. It should disable textarea. expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy(); done(); }); @@ -105,21 +113,27 @@ describe('issue_comment_form component', () => { it('should support quick actions', () => { expect( - vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'), + vm.$el + .querySelector('.js-main-target-form textarea') + .getAttribute('data-supports-quick-actions'), ).toEqual('true'); }); it('should link to markdown docs', () => { const { markdownDocsPath } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual( + 'Markdown', + ); }); it('should link to quick actions docs', () => { const { quickActionsDocsPath } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); + expect( + vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim(), + ).toEqual('quick actions'); }); - it('should resize textarea after note discarded', (done) => { + it('should resize textarea after note discarded', done => { spyOn(Autosize, 'update'); spyOn(vm, 'discard').and.callThrough(); @@ -136,7 +150,9 @@ describe('issue_comment_form component', () => { it('should enter edit mode when arrow up is pressed', () => { spyOn(vm, 'editCurrentUserLastNote').and.callThrough(); vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; - vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(38, true)); + vm.$el + .querySelector('.js-main-target-form textarea') + .dispatchEvent(keyboardDownEvent(38, true)); expect(vm.editCurrentUserLastNote).toHaveBeenCalled(); }); @@ -151,7 +167,9 @@ describe('issue_comment_form component', () => { it('should save note when cmd+enter is pressed', () => { spyOn(vm, 'handleSave').and.callThrough(); vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; - vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true)); + vm.$el + .querySelector('.js-main-target-form textarea') + .dispatchEvent(keyboardDownEvent(13, true)); expect(vm.handleSave).toHaveBeenCalled(); }); @@ -159,7 +177,9 @@ describe('issue_comment_form component', () => { it('should save note when ctrl+enter is pressed', () => { spyOn(vm, 'handleSave').and.callThrough(); vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; - vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + vm.$el + .querySelector('.js-main-target-form textarea') + .dispatchEvent(keyboardDownEvent(13, false, true)); expect(vm.handleSave).toHaveBeenCalled(); }); @@ -168,41 +188,51 @@ describe('issue_comment_form component', () => { describe('actions', () => { it('should be possible to close the issue', () => { - expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close issue'); + expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual( + 'Close issue', + ); }); it('should render comment button as disabled', () => { - expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual('disabled'); + expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual( + 'disabled', + ); }); - it('should enable comment button if it has note', (done) => { + it('should enable comment button if it has note', done => { vm.note = 'Foo'; Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(null); + expect( + vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled'), + ).toEqual(null); done(); }); }); - it('should update buttons texts when it has note', (done) => { + it('should update buttons texts when it has note', done => { vm.note = 'Foo'; Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Comment & close issue'); + expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual( + 'Comment & close issue', + ); expect(vm.$el.querySelector('.js-note-discard')).toBeDefined(); done(); }); }); - it('updates button text with noteable type', (done) => { - vm.noteableType = 'merge_request'; + it('updates button text with noteable type', done => { + vm.noteableType = constants.MERGE_REQUEST_NOTEABLE_TYPE; Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close merge request'); + expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual( + 'Close merge request', + ); done(); }); }); describe('when clicking close/reopen button', () => { - it('should disable button and show a loading spinner', (done) => { + it('should disable button and show a loading spinner', done => { const toggleStateButton = vm.$el.querySelector('.js-action-button'); toggleStateButton.click(); @@ -217,7 +247,7 @@ describe('issue_comment_form component', () => { }); describe('issue is confidential', () => { - it('shows information warning', (done) => { + it('shows information warning', done => { store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true })); Vue.nextTick(() => { expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined(); @@ -237,7 +267,9 @@ describe('issue_comment_form component', () => { }); it('should render signed out widget', () => { - expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual( + 'Please register or sign in to reply', + ); }); it('should not render submission form', () => { diff --git a/spec/javascripts/notes/components/diff_file_header_spec.js b/spec/javascripts/notes/components/diff_file_header_spec.js deleted file mode 100644 index ef6d513444a..00000000000 --- a/spec/javascripts/notes/components/diff_file_header_spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import Vue from 'vue'; -import DiffFileHeader from '~/notes/components/diff_file_header.vue'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -const discussionFixture = 'merge_requests/diff_discussion.json'; - -describe('diff_file_header', () => { - let vm; - const diffDiscussionMock = getJSONFixture(discussionFixture)[0]; - const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file); - const props = { - diffFile, - }; - const Component = Vue.extend(DiffFileHeader); - const selectors = { - get copyButton() { - return vm.$el.querySelector('button[data-original-title="Copy file path to clipboard"]'); - }, - get fileName() { - return vm.$el.querySelector('.file-title-name'); - }, - get titleWrapper() { - return vm.$refs.titleWrapper; - }, - }; - - describe('submodule', () => { - beforeEach(() => { - props.diffFile.submodule = true; - props.diffFile.submoduleLink = '<a href="/bha">Submodule</a>'; - - vm = mountComponent(Component, props); - }); - - it('shows submoduleLink', () => { - expect(selectors.fileName.innerHTML).toBe(props.diffFile.submoduleLink); - }); - - it('has button to copy blob path', () => { - expect(selectors.copyButton).toExist(); - expect(selectors.copyButton.getAttribute('data-clipboard-text')).toBe(props.diffFile.submoduleLink); - }); - }); - - describe('changed file', () => { - beforeEach(() => { - props.diffFile.submodule = false; - props.diffFile.discussionPath = 'some/discussion/id'; - - vm = mountComponent(Component, props); - }); - - it('shows file type icon', () => { - expect(vm.$el.innerHTML).toContain('fa-file-text-o'); - }); - - it('links to discussion path', () => { - expect(selectors.titleWrapper).toExist(); - expect(selectors.titleWrapper.tagName).toBe('A'); - expect(selectors.titleWrapper.getAttribute('href')).toBe(props.diffFile.discussionPath); - }); - - it('shows plain title if no link given', () => { - props.diffFile.discussionPath = undefined; - vm = mountComponent(Component, props); - - expect(selectors.titleWrapper.tagName).not.toBe('A'); - expect(selectors.titleWrapper.href).toBeFalsy(); - }); - - it('has button to copy file path', () => { - expect(selectors.copyButton).toExist(); - expect(selectors.copyButton.getAttribute('data-clipboard-text')).toBe(props.diffFile.filePath); - }); - - it('shows file mode change', (done) => { - vm.diffFile = { - ...props.diffFile, - modeChanged: true, - aMode: '100755', - bMode: '100644', - }; - - Vue.nextTick(() => { - expect( - vm.$refs.fileMode.textContent.trim(), - ).toBe('100755 → 100644'); - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js index f4ec7132dbd..239d7950907 100644 --- a/spec/javascripts/notes/components/diff_with_note_spec.js +++ b/spec/javascripts/notes/components/diff_with_note_spec.js @@ -1,12 +1,14 @@ import Vue from 'vue'; import DiffWithNote from '~/notes/components/diff_with_note.vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import createStore from '~/notes/stores'; +import { mountComponentWithStore } from 'spec/helpers'; const discussionFixture = 'merge_requests/diff_discussion.json'; const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json'; describe('diff_with_note', () => { + let store; let vm; const diffDiscussionMock = getJSONFixture(discussionFixture)[0]; const diffDiscussion = convertObjectPropsToCamelCase(diffDiscussionMock); @@ -29,9 +31,21 @@ describe('diff_with_note', () => { }, }; + beforeEach(() => { + store = createStore(); + store.replaceState({ + ...store.state, + notes: { + noteableData: { + current_user: {}, + }, + }, + }); + }); + describe('text diff', () => { beforeEach(() => { - vm = mountComponent(Component, props); + vm = mountComponentWithStore(Component, { props, store }); }); it('shows text diff', () => { @@ -55,7 +69,7 @@ describe('diff_with_note', () => { }); it('shows image diff', () => { - vm = mountComponent(Component, props); + vm = mountComponentWithStore(Component, { props, store }); expect(selectors.container).toHaveClass('js-image-file'); expect(selectors.diffTable).not.toExist(); diff --git a/spec/javascripts/notes/components/discussion_counter_spec.js b/spec/javascripts/notes/components/discussion_counter_spec.js new file mode 100644 index 00000000000..7b2302e6f47 --- /dev/null +++ b/spec/javascripts/notes/components/discussion_counter_spec.js @@ -0,0 +1,58 @@ +import Vue from 'vue'; +import createStore from '~/notes/stores'; +import DiscussionCounter from '~/notes/components/discussion_counter.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; + +describe('DiscussionCounter component', () => { + let store; + let vm; + + beforeEach(() => { + window.mrTabs = {}; + + const Component = Vue.extend(DiscussionCounter); + + store = createStore(); + store.dispatch('setNoteableData', noteableDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = createComponentWithStore(Component, store); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + describe('jumpToFirstUnresolvedDiscussion', () => { + it('expands unresolved discussion', () => { + spyOn(vm, 'expandDiscussion').and.stub(); + const discussions = [ + { + ...discussionMock, + id: discussionMock.id, + notes: [{ ...discussionMock.notes[0], resolved: true }], + }, + { + ...discussionMock, + id: discussionMock.id + 1, + notes: [{ ...discussionMock.notes[0], resolved: false }], + }, + ]; + const firstDiscussionId = discussionMock.id + 1; + store.replaceState({ + ...store.state, + discussions, + }); + setFixtures(` + <div data-discussion-id="${firstDiscussionId}"></div> + `); + + vm.jumpToFirstUnresolvedDiscussion(); + + expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: firstDiscussionId }); + }); + }); + }); +}); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index c9e549d2096..52cc42cb53d 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -1,14 +1,16 @@ import Vue from 'vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import noteActions from '~/notes/components/note_actions.vue'; import { userDataMock } from '../mock_data'; describe('issue_note_actions component', () => { let vm; + let store; let Component; beforeEach(() => { Component = Vue.extend(noteActions); + store = createStore(); }); afterEach(() => { @@ -27,7 +29,9 @@ describe('issue_note_actions component', () => { canAwardEmoji: true, canReportAsAbuse: true, noteId: 539, - reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', + noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1', + reportAbusePath: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', }; store.dispatch('setUserData', userDataMock); @@ -74,7 +78,9 @@ describe('issue_note_actions component', () => { canAwardEmoji: false, canReportAsAbuse: false, noteId: 539, - reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', + noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1', + reportAbusePath: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', }; vm = new Component({ store, diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index d494c63ff11..7eb4d3aed29 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -3,7 +3,9 @@ import _ from 'underscore'; import Vue from 'vue'; import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; +import createStore from '~/notes/stores'; import '~/behaviors/markdown/render_gfm'; +import { mountComponentWithStore } from 'spec/helpers'; import * as mockData from '../mock_data'; const vueMatchers = { @@ -22,6 +24,7 @@ const vueMatchers = { describe('note_app', () => { let mountComponent; let vm; + let store; beforeEach(() => { jasmine.addMatchers(vueMatchers); @@ -29,16 +32,18 @@ describe('note_app', () => { const IssueNotesApp = Vue.extend(notesApp); - mountComponent = (data) => { + store = createStore(); + mountComponent = data => { const props = data || { noteableData: mockData.noteableDataMock, notesData: mockData.notesDataMock, userData: mockData.userDataMock, }; - return new IssueNotesApp({ - propsData: props, - }).$mount(); + return mountComponentWithStore(IssueNotesApp, { + props, + store, + }); }; }); @@ -48,9 +53,11 @@ describe('note_app', () => { describe('set data', () => { const responseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 200, - })); + next( + request.respondWith(JSON.stringify([]), { + status: 200, + }), + ); }; beforeEach(() => { @@ -74,8 +81,8 @@ describe('note_app', () => { expect(vm.$store.state.userData).toEqual(mockData.userDataMock); }); - it('should fetch notes', () => { - expect(vm.$store.state.notes).toEqual([]); + it('should fetch discussions', () => { + expect(vm.$store.state.discussions).toEqual([]); }); }); @@ -89,15 +96,20 @@ describe('note_app', () => { Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor); }); - it('should render list of notes', (done) => { - const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET['/gitlab-org/gitlab-ce/issues/26/discussions.json'][0].notes[0]; + it('should render list of notes', done => { + const note = + mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[ + '/gitlab-org/gitlab-ce/issues/26/discussions.json' + ][0].notes[0]; setTimeout(() => { expect( vm.$el.querySelector('.main-notes-list .note-header-author-name').textContent.trim(), ).toEqual(note.author.name); - expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual(note.note_html); + expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual( + note.note_html, + ); done(); }, 0); }); @@ -110,9 +122,9 @@ describe('note_app', () => { }); it('should render form comment button as disabled', () => { - expect( - vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled'), - ).toEqual('disabled'); + expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual( + 'disabled', + ); }); }); @@ -135,7 +147,7 @@ describe('note_app', () => { describe('update note', () => { describe('individual note', () => { - beforeEach((done) => { + beforeEach(done => { Vue.http.interceptors.push(mockData.individualNoteInterceptor); spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); @@ -156,7 +168,7 @@ describe('note_app', () => { expect(vm).toIncludeElement('.js-vue-issue-note-form'); }); - it('calls the service to update the note', (done) => { + it('calls the service to update the note', done => { vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; vm.$el.querySelector('.js-vue-issue-save').click(); @@ -169,7 +181,7 @@ describe('note_app', () => { }); describe('discussion note', () => { - beforeEach((done) => { + beforeEach(done => { Vue.http.interceptors.push(mockData.discussionNoteInterceptor); spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); @@ -191,7 +203,7 @@ describe('note_app', () => { expect(vm).toIncludeElement('.js-vue-issue-note-form'); }); - it('updates the note and resets the edit form', (done) => { + it('updates the note and resets the edit form', done => { vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; vm.$el.querySelector('.js-vue-issue-save').click(); @@ -211,12 +223,16 @@ describe('note_app', () => { it('should render markdown docs url', () => { const { markdownDocsPath } = mockData.notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual( + 'Markdown', + ); }); it('should render quick action docs url', () => { const { quickActionsDocsPath } = mockData.notesDataMock; - expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); + expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual( + 'quick actions', + ); }); }); @@ -230,7 +246,7 @@ describe('note_app', () => { Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor); }); - it('should render markdown docs url', (done) => { + it('should render markdown docs url', done => { setTimeout(() => { vm.$el.querySelector('.js-note-edit').click(); const { markdownDocsPath } = mockData.notesDataMock; @@ -244,15 +260,15 @@ describe('note_app', () => { }, 0); }); - it('should not render quick actions docs url', (done) => { + it('should not render quick actions docs url', done => { setTimeout(() => { vm.$el.querySelector('.js-note-edit').click(); const { quickActionsDocsPath } = mockData.notesDataMock; Vue.nextTick(() => { - expect( - vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`), - ).toEqual(null); + expect(vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`)).toEqual( + null, + ); done(); }); }, 0); diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js index 1c30d8691b1..9d98ba219da 100644 --- a/spec/javascripts/notes/components/note_awards_list_spec.js +++ b/spec/javascripts/notes/components/note_awards_list_spec.js @@ -1,15 +1,17 @@ import Vue from 'vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import awardsNote from '~/notes/components/note_awards_list.vue'; import { noteableDataMock, notesDataMock } from '../mock_data'; describe('note_awards_list component', () => { + let store; let vm; let awardsMock; beforeEach(() => { const Component = Vue.extend(awardsNote); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); awardsMock = [ @@ -41,7 +43,9 @@ describe('note_awards_list component', () => { it('should render awarded emojis', () => { expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined(); - expect(vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]')).toBeDefined(); + expect( + vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]'), + ).toBeDefined(); }); it('should be possible to remove awarded emoji', () => { diff --git a/spec/javascripts/notes/components/note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js index 4e551496ff0..efad0785afe 100644 --- a/spec/javascripts/notes/components/note_body_spec.js +++ b/spec/javascripts/notes/components/note_body_spec.js @@ -1,15 +1,16 @@ - import Vue from 'vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import noteBody from '~/notes/components/note_body.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note_body component', () => { + let store; let vm; beforeEach(() => { const Component = Vue.extend(noteBody); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); @@ -37,7 +38,7 @@ describe('issue_note_body component', () => { }); describe('isEditing', () => { - beforeEach((done) => { + beforeEach(done => { vm.isEditing = true; Vue.nextTick(done); }); diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index 413d4f69434..95d400ab3df 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -1,16 +1,18 @@ import Vue from 'vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import issueNoteForm from '~/notes/components/note_form.vue'; import { noteableDataMock, notesDataMock } from '../mock_data'; import { keyboardDownEvent } from '../../issue_show/helpers'; describe('issue_note_form component', () => { + let store; let vm; let props; beforeEach(() => { const Component = Vue.extend(issueNoteForm); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); @@ -31,14 +33,18 @@ describe('issue_note_form component', () => { }); describe('conflicts editing', () => { - it('should show conflict message if note changes outside the component', (done) => { + it('should show conflict message if note changes outside the component', done => { vm.isEditing = true; vm.noteBody = 'Foo'; - const message = 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.'; + const message = + 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.'; Vue.nextTick(() => { expect( - vm.$el.querySelector('.js-conflict-edit-warning').textContent.replace(/\s+/g, ' ').trim(), + vm.$el + .querySelector('.js-conflict-edit-warning') + .textContent.replace(/\s+/g, ' ') + .trim(), ).toEqual(message); done(); }); @@ -47,14 +53,16 @@ describe('issue_note_form component', () => { describe('form', () => { it('should render text area with placeholder', () => { - expect( - vm.$el.querySelector('textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here…'); + expect(vm.$el.querySelector('textarea').getAttribute('placeholder')).toEqual( + 'Write a comment or drag your files here…', + ); }); it('should link to markdown docs', () => { const { markdownDocsPath } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual( + 'Markdown', + ); }); describe('keyboard events', () => { @@ -87,7 +95,7 @@ describe('issue_note_form component', () => { }); describe('actions', () => { - it('should be possible to cancel', (done) => { + it('should be possible to cancel', done => { spyOn(vm, 'cancelHandler').and.callThrough(); vm.isEditing = true; @@ -101,7 +109,7 @@ describe('issue_note_form component', () => { }); }); - it('should be possible to update the note', (done) => { + it('should be possible to update the note', done => { vm.isEditing = true; Vue.nextTick(() => { diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js index 5636f8d1a9f..a3c6bf78988 100644 --- a/spec/javascripts/notes/components/note_header_spec.js +++ b/spec/javascripts/notes/components/note_header_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; import noteHeader from '~/notes/components/note_header.vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; describe('note_header component', () => { + let store; let vm; let Component; beforeEach(() => { Component = Vue.extend(noteHeader); + store = createStore(); }); afterEach(() => { @@ -38,12 +40,8 @@ describe('note_header component', () => { }); it('should render user information', () => { - expect( - vm.$el.querySelector('.note-header-author-name').textContent.trim(), - ).toEqual('Root'); - expect( - vm.$el.querySelector('.note-header-info a').getAttribute('href'), - ).toEqual('/root'); + expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root'); + expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root'); }); it('should render timestamp link', () => { @@ -78,7 +76,7 @@ describe('note_header component', () => { expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined(); }); - it('emits toggle event on click', (done) => { + it('emits toggle event on click', done => { spyOn(vm, '$emit'); vm.$el.querySelector('.js-vue-toggle-button').click(); @@ -89,24 +87,24 @@ describe('note_header component', () => { }); }); - it('renders up arrow when open', (done) => { + it('renders up arrow when open', done => { vm.expanded = true; Vue.nextTick(() => { - expect( - vm.$el.querySelector('.js-vue-toggle-button i').classList, - ).toContain('fa-chevron-up'); + expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( + 'fa-chevron-up', + ); done(); }); }); - it('renders down arrow when closed', (done) => { + it('renders down arrow when closed', done => { vm.expanded = false; Vue.nextTick(() => { - expect( - vm.$el.querySelector('.js-vue-toggle-button i').classList, - ).toContain('fa-chevron-down'); + expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( + 'fa-chevron-down', + ); done(); }); }); diff --git a/spec/javascripts/notes/components/note_signed_out_widget_spec.js b/spec/javascripts/notes/components/note_signed_out_widget_spec.js index 6cba8053888..e217a2caa73 100644 --- a/spec/javascripts/notes/components/note_signed_out_widget_spec.js +++ b/spec/javascripts/notes/components/note_signed_out_widget_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; import noteSignedOut from '~/notes/components/note_signed_out_widget.vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import { notesDataMock } from '../mock_data'; describe('note_signed_out_widget component', () => { + let store; let vm; beforeEach(() => { const Component = Vue.extend(noteSignedOut); + store = createStore(); store.dispatch('setNotesData', notesDataMock); vm = new Component({ @@ -20,18 +22,20 @@ describe('note_signed_out_widget component', () => { }); it('should render sign in link provided in the store', () => { - expect( - vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent, - ).toEqual('sign in'); + expect(vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent).toEqual( + 'sign in', + ); }); it('should render register link provided in the store', () => { - expect( - vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent, - ).toEqual('register'); + expect(vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent).toEqual( + 'register', + ); }); it('should render information text', () => { - expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual( + 'Please register or sign in to reply', + ); }); }); diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index cda550760fe..058ddb6202f 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -1,21 +1,24 @@ import Vue from 'vue'; -import store from '~/notes/stores'; -import issueDiscussion from '~/notes/components/noteable_discussion.vue'; +import createStore from '~/notes/stores'; +import noteableDiscussion from '~/notes/components/noteable_discussion.vue'; +import '~/behaviors/markdown/render_gfm'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; -describe('issue_discussion component', () => { +describe('noteable_discussion component', () => { + let store; let vm; beforeEach(() => { - const Component = Vue.extend(issueDiscussion); + const Component = Vue.extend(noteableDiscussion); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); vm = new Component({ store, propsData: { - note: discussionMock, + discussion: discussionMock, }, }).$mount(); }); @@ -55,4 +58,74 @@ describe('issue_discussion component', () => { ).toBeNull(); }); }); + + describe('computed', () => { + describe('hasMultipleUnresolvedDiscussions', () => { + it('is false if there are no unresolved discussions', done => { + spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([]); + + Vue.nextTick() + .then(() => { + expect(vm.hasMultipleUnresolvedDiscussions).toBe(false); + }) + .then(done) + .catch(done.fail); + }); + + it('is false if there is one unresolved discussion', done => { + spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([discussionMock]); + + Vue.nextTick() + .then(() => { + expect(vm.hasMultipleUnresolvedDiscussions).toBe(false); + }) + .then(done) + .catch(done.fail); + }); + + it('is true if there are two unresolved discussions', done => { + spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([{}, {}]); + + Vue.nextTick() + .then(() => { + expect(vm.hasMultipleUnresolvedDiscussions).toBe(true); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('methods', () => { + describe('jumpToNextDiscussion', () => { + it('expands next unresolved discussion', () => { + spyOn(vm, 'expandDiscussion').and.stub(); + const discussions = [ + discussionMock, + { + ...discussionMock, + id: discussionMock.id + 1, + notes: [{ ...discussionMock.notes[0], resolved: true }], + }, + { + ...discussionMock, + id: discussionMock.id + 2, + notes: [{ ...discussionMock.notes[0], resolved: false }], + }, + ]; + const nextDiscussionId = discussionMock.id + 2; + store.replaceState({ + ...store.state, + discussions, + }); + setFixtures(` + <div data-discussion-id="${nextDiscussionId}"></div> + `); + + vm.jumpToNextDiscussion(); + + expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: nextDiscussionId }); + }); + }); + }); }); diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index 2ffdec7314d..a31d17cacbb 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -1,16 +1,18 @@ import $ from 'jquery'; import _ from 'underscore'; import Vue from 'vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import issueNote from '~/notes/components/noteable_note.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note', () => { + let store; let vm; beforeEach(() => { const Component = Vue.extend(issueNote); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); @@ -27,11 +29,14 @@ describe('issue_note', () => { }); it('should render user information', () => { - expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(note.author.avatar_url); + expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual( + note.author.avatar_url, + ); }); it('should render note header content', () => { - expect(vm.$el.querySelector('.note-header .note-header-author-name').textContent.trim()).toEqual(note.author.name); + const el = vm.$el.querySelector('.note-header .note-header-author-name'); + expect(el.textContent.trim()).toEqual(note.author.name); }); it('should render note actions', () => { @@ -42,7 +47,7 @@ describe('issue_note', () => { expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); }); - it('prevents note preview xss', (done) => { + it('prevents note preview xss', done => { const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`; const alertSpy = spyOn(window, 'alert'); @@ -58,7 +63,7 @@ describe('issue_note', () => { }); describe('cancel edit', () => { - it('restores content of updated note', (done) => { + it('restores content of updated note', done => { const noteBody = 'updated note text'; vm.updateNote = () => Promise.resolve(); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index fa7adc32193..547efa32694 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -51,6 +51,7 @@ export const noteableDataMock = { time_estimate: 0, title: '14', total_time_spent: 0, + noteable_note_url: '/group/project/merge_requests/1#note_1', updated_at: '2017-08-04T09:53:01.226Z', updated_by_id: 1, web_url: '/gitlab-org/gitlab-ce/issues/26', @@ -99,6 +100,8 @@ export const individualNote = { { name: 'art', user: { id: 1, name: 'Root', username: 'root' } }, ], toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji', + noteable_note_url: '/group/project/merge_requests/1#note_1', + note_url: '/group/project/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1', path: '/gitlab-org/gitlab-ce/notes/1390', @@ -157,6 +160,8 @@ export const note = { }, ], toggle_award_path: '/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji', + note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1', path: '/gitlab-org/gitlab-ce/notes/546', @@ -198,6 +203,7 @@ export const discussionMock = { discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', emoji_awardable: true, award_emoji: [], + noteable_note_url: '/group/project/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1', @@ -244,6 +250,7 @@ export const discussionMock = { emoji_awardable: true, award_emoji: [], toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji', + noteable_note_url: '/group/project/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1', path: '/gitlab-org/gitlab-ce/notes/1396', @@ -288,6 +295,7 @@ export const discussionMock = { discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', emoji_awardable: true, award_emoji: [], + noteable_note_url: '/group/project/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1', @@ -335,6 +343,7 @@ export const loggedOutnoteableData = { can_create_note: false, can_update: false, }, + noteable_note_url: '/group/project/merge_requests/1#note_1', create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue', preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue', @@ -469,6 +478,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { }, }, ], + noteable_note_url: '/group/project/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1', @@ -513,6 +523,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790', emoji_awardable: true, award_emoji: [], + noteable_note_url: '/group/project/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1', @@ -567,6 +578,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052', emoji_awardable: true, award_emoji: [], + noteable_note_url: '/group/project/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1', @@ -618,6 +630,7 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = { emoji_awardable: true, award_emoji: [], toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji', + noteable_note_url: '/group/project/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1', path: '/gitlab-org/gitlab-ce/notes/1471', diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 520a25cc5c6..985c2f81ef3 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import _ from 'underscore'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import * as actions from '~/notes/stores/actions'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import testAction from '../../helpers/vuex_action_helper'; import { resetStore } from '../helpers'; import { @@ -14,6 +14,12 @@ import { } from '../mock_data'; describe('Actions Notes Store', () => { + let store; + + beforeEach(() => { + store = createStore(); + }); + afterEach(() => { resetStore(store); }); @@ -76,7 +82,7 @@ describe('Actions Notes Store', () => { actions.setInitialNotes, [individualNote], { notes: [] }, - [{ type: 'SET_INITIAL_NOTES', payload: [individualNote] }], + [{ type: 'SET_INITIAL_DISCUSSIONS', payload: [individualNote] }], [], done, ); @@ -109,6 +115,19 @@ describe('Actions Notes Store', () => { }); }); + describe('expandDiscussion', () => { + it('should expand discussion', done => { + testAction( + actions.expandDiscussion, + { discussionId: discussionMock.id }, + { notes: [discussionMock] }, + [{ type: 'EXPAND_DISCUSSION', payload: { discussionId: discussionMock.id } }], + [], + done, + ); + }); + }); + describe('async methods', () => { const interceptor = (request, next) => { next( @@ -194,7 +213,14 @@ describe('Actions Notes Store', () => { }); it('sets issue state as reopened', done => { - testAction(actions.toggleIssueLocalState, 'reopened', {}, [{ type: 'REOPEN_ISSUE' }], [], done); + testAction( + actions.toggleIssueLocalState, + 'reopened', + {}, + [{ type: 'REOPEN_ISSUE' }], + [], + done, + ); }); }); @@ -239,13 +265,7 @@ describe('Actions Notes Store', () => { .dispatch('poll') .then(() => new Promise(resolve => requestAnimationFrame(resolve))) .then(() => { - expect(Vue.http.get).toHaveBeenCalledWith(jasmine.anything(), { - url: jasmine.anything(), - method: 'get', - headers: { - 'X-Last-Fetched-At': undefined, - }, - }); + expect(Vue.http.get).toHaveBeenCalled(); expect(store.state.lastFetchedAt).toBe('123456'); jasmine.clock().tick(1500); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index e5550580bf8..5501e50e97b 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -1,12 +1,18 @@ import * as getters from '~/notes/stores/getters'; -import { notesDataMock, userDataMock, noteableDataMock, individualNote, collapseNotesMock } from '../mock_data'; +import { + notesDataMock, + userDataMock, + noteableDataMock, + individualNote, + collapseNotesMock, +} from '../mock_data'; describe('Getters Notes Store', () => { let state; beforeEach(() => { state = { - notes: [individualNote], + discussions: [individualNote], targetNoteHash: 'hash', lastFetchedAt: 'timestamp', @@ -15,15 +21,15 @@ describe('Getters Notes Store', () => { noteableData: noteableDataMock, }; }); - describe('notes', () => { - it('should return all notes in the store', () => { - expect(getters.notes(state)).toEqual([individualNote]); + describe('discussions', () => { + it('should return all discussions in the store', () => { + expect(getters.discussions(state)).toEqual([individualNote]); }); }); describe('Collapsed notes', () => { const stateCollapsedNotes = { - notes: collapseNotesMock, + discussions: collapseNotesMock, targetNoteHash: 'hash', lastFetchedAt: 'timestamp', @@ -33,7 +39,7 @@ describe('Getters Notes Store', () => { }; it('should return a single system note when a description was updated multiple times', () => { - expect(getters.notes(stateCollapsedNotes).length).toEqual(1); + expect(getters.discussions(stateCollapsedNotes).length).toEqual(1); }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 98f101d6bc5..556a1c244c0 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -1,5 +1,12 @@ import mutations from '~/notes/stores/mutations'; -import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; +import { + note, + discussionMock, + notesDataMock, + userDataMock, + noteableDataMock, + individualNote, +} from '../mock_data'; describe('Notes Store mutations', () => { describe('ADD_NEW_NOTE', () => { @@ -7,7 +14,7 @@ describe('Notes Store mutations', () => { let noteData; beforeEach(() => { - state = { notes: [] }; + state = { discussions: [] }; noteData = { expanded: true, id: note.discussion_id, @@ -20,46 +27,60 @@ describe('Notes Store mutations', () => { it('should add a new note to an array of notes', () => { expect(state).toEqual({ - notes: [noteData], + discussions: [noteData], }); - expect(state.notes.length).toBe(1); + expect(state.discussions.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); + expect(state.discussions.length).toBe(1); }); }); describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { it('should add a reply to a specific discussion', () => { - const state = { notes: [discussionMock] }; + const state = { discussions: [discussionMock] }; const newReply = Object.assign({}, note, { discussion_id: discussionMock.id }); mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); - expect(state.notes[0].notes.length).toEqual(4); + expect(state.discussions[0].notes.length).toEqual(4); }); }); describe('DELETE_NOTE', () => { it('should delete a note ', () => { - const state = { notes: [discussionMock] }; + const state = { discussions: [discussionMock] }; const toDelete = discussionMock.notes[0]; const lengthBefore = discussionMock.notes.length; mutations.DELETE_NOTE(state, toDelete); - expect(state.notes[0].notes.length).toEqual(lengthBefore - 1); + expect(state.discussions[0].notes.length).toEqual(lengthBefore - 1); + }); + }); + + describe('EXPAND_DISCUSSION', () => { + it('should expand a collapsed discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: false }); + + const state = { + discussions: [discussion], + }; + + mutations.EXPAND_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.discussions[0].expanded).toEqual(true); }); }); describe('REMOVE_PLACEHOLDER_NOTES', () => { it('should remove all placeholder notes in indivudal notes and discussion', () => { const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true }); - const state = { notes: [placeholderNote] }; + const state = { discussions: [placeholderNote] }; mutations.REMOVE_PLACEHOLDER_NOTES(state); - expect(state.notes).toEqual([]); + expect(state.discussions).toEqual([]); }); }); @@ -96,26 +117,29 @@ describe('Notes Store mutations', () => { }); }); - describe('SET_INITIAL_NOTES', () => { + describe('SET_INITIAL_DISCUSSIONS', () => { it('should set the initial notes received', () => { const state = { - notes: [], + discussions: [], }; const legacyNote = { id: 2, individual_note: true, - notes: [{ - note: '1', - }, { - note: '2', - }], + notes: [ + { + note: '1', + }, + { + note: '2', + }, + ], }; - mutations.SET_INITIAL_NOTES(state, [note, legacyNote]); - expect(state.notes[0].id).toEqual(note.id); - expect(state.notes[1].notes[0].note).toBe(legacyNote.notes[0].note); - expect(state.notes[2].notes[0].note).toBe(legacyNote.notes[1].note); - expect(state.notes.length).toEqual(3); + mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]); + expect(state.discussions[0].id).toEqual(note.id); + expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note); + expect(state.discussions[2].notes[0].note).toBe(legacyNote.notes[1].note); + expect(state.discussions.length).toEqual(3); }); }); @@ -144,17 +168,17 @@ describe('Notes Store mutations', () => { describe('SHOW_PLACEHOLDER_NOTE', () => { it('should set a placeholder note', () => { const state = { - notes: [], + discussions: [], }; mutations.SHOW_PLACEHOLDER_NOTE(state, note); - expect(state.notes[0].isPlaceholderNote).toEqual(true); + expect(state.discussions[0].isPlaceholderNote).toEqual(true); }); }); describe('TOGGLE_AWARD', () => { it('should add award if user has not reacted yet', () => { const state = { - notes: [note], + discussions: [note], userData: userDataMock, }; @@ -164,9 +188,9 @@ describe('Notes Store mutations', () => { }; mutations.TOGGLE_AWARD(state, data); - const lastIndex = state.notes[0].award_emoji.length - 1; + const lastIndex = state.discussions[0].award_emoji.length - 1; - expect(state.notes[0].award_emoji[lastIndex]).toEqual({ + expect(state.discussions[0].award_emoji[lastIndex]).toEqual({ name: 'cartwheel', user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username }, }); @@ -174,7 +198,7 @@ describe('Notes Store mutations', () => { it('should remove award if user already reacted', () => { const state = { - notes: [note], + discussions: [note], userData: { id: 1, name: 'Administrator', @@ -187,7 +211,7 @@ describe('Notes Store mutations', () => { awardName: 'bath_tone3', }; mutations.TOGGLE_AWARD(state, data); - expect(state.notes[0].award_emoji.length).toEqual(2); + expect(state.discussions[0].award_emoji.length).toEqual(2); }); }); @@ -196,43 +220,43 @@ describe('Notes Store mutations', () => { const discussion = Object.assign({}, discussionMock, { expanded: false }); const state = { - notes: [discussion], + discussions: [discussion], }; mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id }); - expect(state.notes[0].expanded).toEqual(true); + expect(state.discussions[0].expanded).toEqual(true); }); it('should close a opened discussion', () => { const state = { - notes: [discussionMock], + discussions: [discussionMock], }; mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id }); - expect(state.notes[0].expanded).toEqual(false); + expect(state.discussions[0].expanded).toEqual(false); }); }); describe('UPDATE_NOTE', () => { it('should update a note', () => { const state = { - notes: [individualNote], + discussions: [individualNote], }; const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' }); mutations.UPDATE_NOTE(state, updated); - expect(state.notes[0].notes[0].note).toEqual('Foo'); + expect(state.discussions[0].notes[0].note).toEqual('Foo'); }); }); describe('CLOSE_ISSUE', () => { it('should set issue as closed', () => { const state = { - notes: [], + discussions: [], targetNoteHash: null, lastFetchedAt: null, isToggleStateButtonLoading: false, @@ -249,7 +273,7 @@ describe('Notes Store mutations', () => { describe('REOPEN_ISSUE', () => { it('should set issue as closed', () => { const state = { - notes: [], + discussions: [], targetNoteHash: null, lastFetchedAt: null, isToggleStateButtonLoading: false, @@ -266,7 +290,7 @@ describe('Notes Store mutations', () => { describe('TOGGLE_STATE_BUTTON_LOADING', () => { it('should set isToggleStateButtonLoading as true', () => { const state = { - notes: [], + discussions: [], targetNoteHash: null, lastFetchedAt: null, isToggleStateButtonLoading: false, @@ -281,7 +305,7 @@ describe('Notes Store mutations', () => { it('should set isToggleStateButtonLoading as false', () => { const state = { - notes: [], + discussions: [], targetNoteHash: null, lastFetchedAt: null, isToggleStateButtonLoading: true, diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 2854263a25a..faeedae40e9 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -35,11 +35,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('Notes', function() { const FLASH_TYPE_ALERT = 'alert'; const NOTES_POST_PATH = /(.*)\/notes\?html=true$/; - var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw'; - preloadFixtures(commentsTemplate); + var fixture = 'snippets/show.html.raw'; + preloadFixtures(fixture); beforeEach(function() { - loadFixtures(commentsTemplate); + loadFixtures(fixture); gl.utils.disableButtonIfEmptyField = _.noop; window.project_uploads_path = 'http://test.host/uploads'; $('body').attr('data-page', 'projects:merge_requets:show'); @@ -65,16 +65,9 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; let mock; beforeEach(function() { - spyOn(axios, 'patch').and.callThrough(); + spyOn(axios, 'patch').and.callFake(() => new Promise(() => {})); mock = new MockAdapter(axios); - - mock - .onPatch( - `${ - gl.TEST_HOST - }/frontend-fixtures/merge-requests-project/merge_requests/1.json`, - ) - .reply(200, {}); + mock.onAny().reply(200, {}); $('.js-comment-button').on('click', function(e) { e.preventDefault(); @@ -90,26 +83,17 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; const changeEvent = document.createEvent('HTMLEvents'); changeEvent.initEvent('change', true, true); $('input[type=checkbox]') - .attr('checked', true)[1] + .attr('checked', true)[0] .dispatchEvent(changeEvent); - expect($('.js-task-list-field.original-task-list').val()).toBe( - '- [x] Task List Item', - ); + expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item'); }); it('submits an ajax request on tasklist:changed', function(done) { $('.js-task-list-container').trigger('tasklist:changed'); setTimeout(() => { - expect(axios.patch).toHaveBeenCalledWith( - `${ - gl.TEST_HOST - }/frontend-fixtures/merge-requests-project/merge_requests/1.json`, - { - note: { note: '' }, - }, - ); + expect(axios.patch).toHaveBeenCalled(); done(); }); }); @@ -200,9 +184,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; updatedNote.note = 'bar'; this.notes.updateNote(updatedNote, $targetNote); - expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith( - $targetNote, - ); + expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote); expect(this.notes.setupNewNote).toHaveBeenCalled(); done(); @@ -282,10 +264,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; Notes.isNewNote.and.returnValue(true); Notes.prototype.renderNote.call(notes, note, null, $notesList); - expect(Notes.animateAppendNote).toHaveBeenCalledWith( - note.html, - $notesList, - ); + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList); }); }); @@ -300,10 +279,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; Notes.prototype.renderNote.call(notes, note, null, $notesList); - expect(Notes.animateUpdateNote).toHaveBeenCalledWith( - note.html, - $note, - ); + expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note); expect(notes.setupNewNote).toHaveBeenCalledWith($newNote); }); @@ -331,10 +307,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; $notesList.find.and.returnValue($note); Notes.prototype.renderNote.call(notes, note, null, $notesList); - expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith( - note, - $note, - ); + expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note); }); }); }); @@ -400,10 +373,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; $form.length = 1; row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']); - notes = jasmine.createSpyObj('notes', [ - 'isParallelView', - 'updateNotesCount', - ]); + notes = jasmine.createSpyObj('notes', ['isParallelView', 'updateNotesCount']); notes.note_ids = []; spyOn(Notes, 'isNewNote'); @@ -464,10 +434,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('should call Notes.animateAppendNote', () => { - expect(Notes.animateAppendNote).toHaveBeenCalledWith( - note.html, - discussionContainer, - ); + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer); }); }); }); @@ -571,9 +538,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; mockNotesPost(); $('.js-comment-button').click(); - expect($notesContainer.find('.note.being-posted').length > 0).toEqual( - true, - ); + expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true); }); it('should remove placeholder note when new comment is done posting', done => { @@ -617,9 +582,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; $('.js-comment-button').click(); setTimeout(() => { - expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual( - true, - ); + expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true); done(); }); @@ -734,14 +697,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough(); $('.js-comment-button').click(); - expect( - $notesContainer.find('.system-note.being-posted').length, - ).toEqual(1); // Placeholder shown + expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown setTimeout(() => { - expect( - $notesContainer.find('.system-note.being-posted').length, - ).toEqual(0); // Placeholder removed + expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed done(); }); }); @@ -815,9 +774,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; it('should return form metadata object from form reference', () => { $form.find('textarea.js-note-text').val(sampleComment); - const { formData, formContent, formAction } = this.notes.getFormData( - $form, - ); + const { formData, formContent, formAction } = this.notes.getFormData($form); expect(formData.indexOf(sampleComment) > -1).toBe(true); expect(formContent).toEqual(sampleComment); @@ -833,9 +790,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; const { formContent } = this.notes.getFormData($form); expect(_.escape).toHaveBeenCalledWith(sampleComment); - expect(formContent).toEqual( - '<script>alert("Boom!");</script>', - ); + expect(formContent).toEqual('<script>alert("Boom!");</script>'); }); }); @@ -845,8 +800,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('should return true when comment begins with a quick action', () => { - const sampleComment = - '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; + const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; const hasQuickActions = this.notes.hasQuickActions(sampleComment); expect(hasQuickActions).toBeTruthy(); @@ -870,8 +824,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('stripQuickActions', () => { it('should strip quick actions from the comment which begins with a quick action', () => { this.notes = new Notes(); - const sampleComment = - '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; + const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; const stripedComment = this.notes.stripQuickActions(sampleComment); expect(stripedComment).toBe(''); @@ -879,8 +832,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; it('should strip quick actions from the comment but leaves plain comment if it is present', () => { this.notes = new Notes(); - const sampleComment = - '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this'; + const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this'; const stripedComment = this.notes.stripQuickActions(sampleComment); expect(stripedComment).toBe('Merging this'); @@ -888,8 +840,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; it('should NOT strip string that has slashes within', () => { this.notes = new Notes(); - const sampleComment = - 'http://127.0.0.1:3000/root/gitlab-shell/issues/1'; + const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1'; const stripedComment = this.notes.stripQuickActions(sampleComment); expect(stripedComment).toBe(sampleComment); @@ -909,29 +860,21 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; it('should return executing quick action description when note has single quick action', () => { const sampleComment = '/close'; - expect( - this.notes.getQuickActionDescription( - sampleComment, - availableQuickActions, - ), - ).toBe('Applying command to close this issue'); + expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe( + 'Applying command to close this issue', + ); }); it('should return generic multiple quick action description when note has multiple quick actions', () => { const sampleComment = '/close\n/title [Duplicate] Issue foobar'; - expect( - this.notes.getQuickActionDescription( - sampleComment, - availableQuickActions, - ), - ).toBe('Applying multiple commands'); + expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe( + 'Applying multiple commands', + ); }); it('should return generic quick action description when available quick actions list is not populated', () => { const sampleComment = '/close\n/title [Duplicate] Issue foobar'; - expect(this.notes.getQuickActionDescription(sampleComment)).toBe( - 'Applying command', - ); + expect(this.notes.getQuickActionDescription(sampleComment)).toBe('Applying command'); }); }); @@ -961,17 +904,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; expect($tempNote.attr('id')).toEqual(uniqueId); expect($tempNote.hasClass('being-posted')).toBeTruthy(); expect($tempNote.hasClass('fade-in-half')).toBeTruthy(); - $tempNote - .find('.timeline-icon > a, .note-header-info > a') - .each(function() { - expect($(this).attr('href')).toEqual(`/${currentUsername}`); - }); - expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual( - currentUserAvatar, - ); - expect( - $tempNote.find('.timeline-content').hasClass('discussion'), - ).toBeFalsy(); + $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() { + expect($(this).attr('href')).toEqual(`/${currentUsername}`); + }); + expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar); + expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy(); expect( $tempNoteHeader .find('.d-none.d-sm-inline-block') @@ -1002,9 +939,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); expect($tempNote.prop('nodeName')).toEqual('LI'); - expect( - $tempNote.find('.timeline-content').hasClass('discussion'), - ).toBeTruthy(); + expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); }); it('should return a escaped user name', () => { @@ -1061,11 +996,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('shows a flash message', () => { - this.notes.addFlash( - 'Error message', - FLASH_TYPE_ALERT, - this.notes.parentTimeline.get(0), - ); + this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0)); expect($('.flash-alert').is(':visible')).toBeTruthy(); }); @@ -1078,11 +1009,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); it('hides visible flash message', () => { - this.notes.addFlash( - 'Error message 1', - FLASH_TYPE_ALERT, - this.notes.parentTimeline.get(0), - ); + this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0)); this.notes.clearFlash(); diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index 68043b91bd0..78d8e9e572e 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -4,7 +4,7 @@ import eventHub from '~/pipelines/event_hub'; describe('Pipelines Table Row', () => { const jsonFixtureName = 'pipelines/pipelines.json'; - const buildComponent = (pipeline) => { + const buildComponent = pipeline => { const PipelinesTableRowComponent = Vue.extend(tableRowComp); return new PipelinesTableRowComponent({ el: document.querySelector('.test-dom-element'), @@ -52,9 +52,9 @@ describe('Pipelines Table Row', () => { }); it('should render status text', () => { - expect( - component.$el.querySelector('.table-section.commit-link a').textContent, - ).toContain(pipeline.details.status.text); + expect(component.$el.querySelector('.table-section.commit-link a').textContent).toContain( + pipeline.details.status.text, + ); }); }); @@ -78,11 +78,15 @@ describe('Pipelines Table Row', () => { describe('when a user is provided', () => { it('should render user information', () => { expect( - component.$el.querySelector('.table-section:nth-child(2) a:nth-child(3)').getAttribute('href'), + component.$el + .querySelector('.table-section:nth-child(2) a:nth-child(3)') + .getAttribute('href'), ).toEqual(pipeline.user.path); expect( - component.$el.querySelector('.table-section:nth-child(2) img').getAttribute('data-original-title'), + component.$el + .querySelector('.table-section:nth-child(2) img') + .getAttribute('data-original-title'), ).toEqual(pipeline.user.name); }); }); @@ -105,7 +109,9 @@ describe('Pipelines Table Row', () => { } const commitAuthorLink = commitAuthorElement.getAttribute('href'); - const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('data-original-title'); + const commitAuthorName = commitAuthorElement + .querySelector('img.avatar') + .getAttribute('data-original-title'); return { commitAuthorElement, commitAuthorLink, commitAuthorName }; }; @@ -145,7 +151,8 @@ describe('Pipelines Table Row', () => { it('should render an icon for each stage', () => { expect( - component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button').length, + component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button') + .length, ).toEqual(pipeline.details.stages.length); }); }); @@ -167,7 +174,7 @@ describe('Pipelines Table Row', () => { }); it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => { - eventHub.$on('retryPipeline', (endpoint) => { + eventHub.$on('retryPipeline', endpoint => { expect(endpoint).toEqual('/retry'); }); @@ -176,7 +183,7 @@ describe('Pipelines Table Row', () => { }); it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => { - eventHub.$on('openConfirmationModal', (data) => { + eventHub.$once('openConfirmationModal', data => { expect(data.endpoint).toEqual('/cancel'); expect(data.pipelineId).toEqual(pipeline.id); }); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index b10d8be6781..a4753ab7cde 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -4,71 +4,102 @@ import ShortcutsIssuable from '~/shortcuts_issuable'; initCopyAsGFM(); -describe('ShortcutsIssuable', function () { - const fixtureName = 'merge_requests/diff_comment.html.raw'; +const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form'; + +describe('ShortcutsIssuable', function() { + const fixtureName = 'snippets/show.html.raw'; preloadFixtures(fixtureName); + beforeEach(() => { loadFixtures(fixtureName); + $('body').append( + `<div class="js-main-target-form"> + <textare class="js-vue-comment-form"></textare> + </div>`, + ); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); this.shortcut = new ShortcutsIssuable(true); }); + + afterEach(() => { + $(FORM_SELECTOR).remove(); + }); + describe('replyWithSelectedText', () => { // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. - const stubSelection = (html) => { + const stubSelection = html => { window.gl.utils.getSelectedFragment = () => { const node = document.createElement('div'); node.innerHTML = html; + return node; }; }; - beforeEach(() => { - this.selector = '.js-main-target-form #note_note'; - }); describe('with empty selection', () => { it('does not return an error', () => { - this.shortcut.replyWithSelectedText(true); - expect($(this.selector).val()).toBe(''); + ShortcutsIssuable.replyWithSelectedText(true); + + expect($(FORM_SELECTOR).val()).toBe(''); }); + it('triggers `focus`', () => { - this.shortcut.replyWithSelectedText(true); - expect(document.activeElement).toBe(document.querySelector(this.selector)); + const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); + ShortcutsIssuable.replyWithSelectedText(true); + + expect(spy).toHaveBeenCalled(); }); }); + describe('with any selection', () => { beforeEach(() => { stubSelection('<p>Selected text.</p>'); }); + it('leaves existing input intact', () => { - $(this.selector).val('This text was already here.'); - expect($(this.selector).val()).toBe('This text was already here.'); - this.shortcut.replyWithSelectedText(true); - expect($(this.selector).val()).toBe('This text was already here.\n\n> Selected text.\n\n'); + $(FORM_SELECTOR).val('This text was already here.'); + expect($(FORM_SELECTOR).val()).toBe('This text was already here.'); + + ShortcutsIssuable.replyWithSelectedText(true); + expect($(FORM_SELECTOR).val()).toBe('This text was already here.\n\n> Selected text.\n\n'); }); + it('triggers `input`', () => { let triggered = false; - $(this.selector).on('input', () => { + $(FORM_SELECTOR).on('input', () => { triggered = true; }); - this.shortcut.replyWithSelectedText(true); + + ShortcutsIssuable.replyWithSelectedText(true); expect(triggered).toBe(true); }); + it('triggers `focus`', () => { - this.shortcut.replyWithSelectedText(true); - expect(document.activeElement).toBe(document.querySelector(this.selector)); + const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus'); + ShortcutsIssuable.replyWithSelectedText(true); + + expect(spy).toHaveBeenCalled(); }); }); + describe('with a one-line selection', () => { it('quotes the selection', () => { stubSelection('<p>This text has been selected.</p>'); - this.shortcut.replyWithSelectedText(true); - expect($(this.selector).val()).toBe('> This text has been selected.\n\n'); + ShortcutsIssuable.replyWithSelectedText(true); + + expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n'); }); }); + describe('with a multi-line selection', () => { it('quotes the selected lines as a group', () => { - stubSelection('<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>'); - this.shortcut.replyWithSelectedText(true); - expect($(this.selector).val()).toBe('> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n'); + stubSelection( + '<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>', + ); + ShortcutsIssuable.replyWithSelectedText(true); + + expect($(FORM_SELECTOR).val()).toBe( + '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n', + ); }); }); }); diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js index ee92295ef5e..94cded7ee37 100644 --- a/spec/javascripts/shortcuts_spec.js +++ b/spec/javascripts/shortcuts_spec.js @@ -2,10 +2,11 @@ import $ from 'jquery'; import Shortcuts from '~/shortcuts'; describe('Shortcuts', () => { - const fixtureName = 'merge_requests/diff_comment.html.raw'; - const createEvent = (type, target) => $.Event(type, { - target, - }); + const fixtureName = 'snippets/show.html.raw'; + const createEvent = (type, target) => + $.Event(type, { + target, + }); preloadFixtures(fixtureName); @@ -21,19 +22,19 @@ describe('Shortcuts', () => { it('focuses preview button in form', () => { Shortcuts.toggleMarkdownPreview( - createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text'), - )); + 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) => { + 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'), - )); + 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'); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 994011b262a..2626b439ca6 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -91,7 +91,8 @@ testsContext.keys().forEach(function(path) { try { testsContext(path); } catch (err) { - console.error('[ERROR] Unable to load spec: ', path); + console.log(err); + console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path); describe('Test bundle', function() { it(`includes '${path}'`, function() { expect(err).toBeNull(); diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js index ba8ab0b2cd7..7e57c51bf29 100644 --- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; import { userDataMock } from '../../../notes/mock_data'; describe('issue placeholder system note component', () => { + let store; let vm; beforeEach(() => { const Component = Vue.extend(issuePlaceholderNote); + store = createStore(); store.dispatch('setUserData', userDataMock); vm = new Component({ store, @@ -21,15 +23,23 @@ describe('issue placeholder system note component', () => { describe('user information', () => { it('should render user avatar with link', () => { - expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); - expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url); + expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual( + userDataMock.path, + ); + expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual( + userDataMock.avatar_url, + ); }); }); describe('note content', () => { it('should render note header information', () => { - expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path); - expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`); + expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual( + userDataMock.path, + ); + expect( + vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim(), + ).toEqual(`@${userDataMock.username}`); }); it('should render note body', () => { diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js index 36aaf0a6c2e..aa4c9c4c88c 100644 --- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js @@ -1,8 +1,8 @@ import Vue from 'vue'; import issueSystemNote from '~/vue_shared/components/notes/system_note.vue'; -import store from '~/notes/stores'; +import createStore from '~/notes/stores'; -describe('issue system note', () => { +describe('system note component', () => { let vm; let props; @@ -24,6 +24,7 @@ describe('issue system note', () => { }, }; + const store = createStore(); store.dispatch('setTargetNoteHash', `note_${props.note.id}`); const Component = Vue.extend(issueSystemNote); @@ -49,9 +50,10 @@ describe('issue system note', () => { expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined(); }); - it('should render note header component', () => { - expect( - vm.$el.querySelector('.system-note-message').innerHTML, - ).toEqual(props.note.note_html); + // Redcarpet Markdown renderer wraps text in `<p>` tags + // we need to strip them because they break layout of commit lists in system notes: + // https://gitlab.com/gitlab-org/gitlab-ce/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png + it('removes wrapping paragraph from note HTML', () => { + expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>'); }); }); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 7fe3bd92049..bdeebe0de75 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,11 +1,12 @@ import $ from 'jquery'; -import Mousetrap from 'mousetrap'; import Dropzone from 'dropzone'; +import Mousetrap from 'mousetrap'; import ZenMode from '~/zen_mode'; describe('ZenMode', () => { let zen; - const fixtureName = 'merge_requests/merge_request_with_comment.html.raw'; + let dropzoneForElementSpy; + const fixtureName = 'snippets/show.html.raw'; preloadFixtures(fixtureName); @@ -18,15 +19,17 @@ describe('ZenMode', () => { } function escapeKeydown() { - $('.notes-form textarea').trigger($.Event('keydown', { - keyCode: 27, - })); + $('.notes-form textarea').trigger( + $.Event('keydown', { + keyCode: 27, + }), + ); } beforeEach(() => { loadFixtures(fixtureName); - spyOn(Dropzone, 'forElement').and.callFake(() => ({ + dropzoneForElementSpy = spyOn(Dropzone, 'forElement').and.callFake(() => ({ enable: () => true, })); zen = new ZenMode(); @@ -35,11 +38,29 @@ describe('ZenMode', () => { zen.scroll_position = 456; }); + describe('enabling dropzone', () => { + beforeEach(() => { + enterZen(); + }); + + it('should not call dropzone if element is not dropzone valid', () => { + $('.div-dropzone').addClass('js-invalid-dropzone'); + exitZen(); + expect(dropzoneForElementSpy.calls.count()).toEqual(0); + }); + + it('should call dropzone if element is dropzone valid', () => { + $('.div-dropzone').removeClass('js-invalid-dropzone'); + exitZen(); + expect(dropzoneForElementSpy.calls.count()).toEqual(2); + }); + }); + describe('on enter', () => { it('pauses Mousetrap', () => { - spyOn(Mousetrap, 'pause'); + const mouseTrapPauseSpy = spyOn(Mousetrap, 'pause'); enterZen(); - expect(Mousetrap.pause).toHaveBeenCalled(); + expect(mouseTrapPauseSpy).toHaveBeenCalled(); }); it('removes textarea styling', () => { @@ -62,9 +83,9 @@ describe('ZenMode', () => { beforeEach(enterZen); it('unpauses Mousetrap', () => { - spyOn(Mousetrap, 'unpause'); + const mouseTrapUnpauseSpy = spyOn(Mousetrap, 'unpause'); exitZen(); - expect(Mousetrap.unpause).toHaveBeenCalled(); + expect(mouseTrapUnpauseSpy).toHaveBeenCalled(); }); it('restores the scroll position', () => { @@ -73,22 +94,4 @@ describe('ZenMode', () => { expect(zen.scrollTo).toHaveBeenCalled(); }); }); - - describe('enabling dropzone', () => { - beforeEach(() => { - enterZen(); - }); - - it('should not call dropzone if element is not dropzone valid', () => { - $('.div-dropzone').addClass('js-invalid-dropzone'); - exitZen(); - expect(Dropzone.forElement).not.toHaveBeenCalled(); - }); - - it('should call dropzone if element is dropzone valid', () => { - $('.div-dropzone').removeClass('js-invalid-dropzone'); - exitZen(); - expect(Dropzone.forElement).toHaveBeenCalled(); - }); - }); }); |