diff options
4 files changed, 489 insertions, 756 deletions
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 69ec6ab8600..bfcc726a030 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -57,26 +57,12 @@ export default { required: true, }, }, - data() { - return { - blobForkSuggestion: null, - }; - }, computed: { ...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']), - hasExpandedDiscussions() { - return this.diffHasExpandedDiscussions(this.diffFile); - }, diffContentIDSelector() { return `#diff-content-${this.diffFile.file_hash}`; }, - icon() { - if (this.diffFile.submodule) { - return 'archive'; - } - return this.diffFile.blob.icon; - }, titleLink() { if (this.diffFile.submodule) { return this.diffFile.submodule_tree_url || this.diffFile.submodule_link; @@ -99,9 +85,6 @@ export default { return this.diffFile.file_path; }, - titleTag() { - return this.diffFile.file_hash ? 'a' : 'span'; - }, isUsingLfs() { return this.diffFile.stored_externally && this.diffFile.external_storage === 'lfs'; }, @@ -135,9 +118,6 @@ export default { isModeChanged() { return this.diffFile.viewer.name === diffViewerModes.mode_changed; }, - showExpandDiffToFullFileEnabled() { - return gon.features.expandDiffFullFile && !this.diffFile.is_fully_expanded; - }, expandDiffToFullFileTitle() { if (this.diffFile.isShowingFullFile) { return s__('MRDiff|Show changes only'); @@ -156,21 +136,12 @@ export default { 'toggleFileDiscussionWrappers', 'toggleFullDiff', ]), - handleToggleFile(e, checkTarget) { - if ( - !checkTarget || - e.target === this.$refs.header || - (e.target.classList && e.target.classList.contains('diff-toggle-caret')) - ) { - this.$emit('toggleFile'); - } + handleToggleFile() { + this.$emit('toggleFile'); }, showForkMessage() { this.$emit('showForkMessage'); }, - handleToggleDiscussions() { - this.toggleFileDiscussionWrappers(this.diffFile); - }, handleFileNameClick(e) { const isLinkToOtherPage = this.diffFile.submodule_tree_url || this.diffFile.submodule_link || this.discussionPath; @@ -178,7 +149,6 @@ export default { if (!isLinkToOtherPage) { e.preventDefault(); const selector = this.diffContentIDSelector; - scrollToElement(document.querySelector(selector)); window.location.hash = selector; } @@ -191,22 +161,23 @@ export default { <div ref="header" class="js-file-title file-title file-title-flex-parent" - @click="handleToggleFile($event, true)" + @click.self="handleToggleFile" > <div class="file-header-content"> <icon v-if="collapsible" + ref="collapseIcon" :name="collapseIcon" :size="16" aria-hidden="true" class="diff-toggle-caret append-right-5" - @click.stop="handleToggle" + @click.stop="handleToggleFile" /> <a v-once id="diffFile.file_path" ref="titleWrapper" - class="append-right-4 js-title-wrapper" + class="append-right-4" :href="titleLink" @click="handleFileNameClick" > @@ -214,7 +185,7 @@ export default { :file-name="filePath" :size="18" aria-hidden="true" - css-classes="js-file-icon append-right-5" + css-classes="append-right-5" /> <span v-if="isFileRenamed"> <strong @@ -260,12 +231,13 @@ export default { <template v-if="diffFile.blob && diffFile.blob.readable_text"> <span v-gl-tooltip.hover :title="s__('MergeRequests|Toggle comments for this file')"> <gl-button + ref="toggleDiscussionsButton" :disabled="!diffHasDiscussions(diffFile)" - :class="{ active: hasExpandedDiscussions }" + :class="{ active: diffHasExpandedDiscussions(diffFile) }" class="js-btn-vue-toggle-comments btn" data-qa-selector="toggle_comments_button" type="button" - @click="handleToggleDiscussions" + @click="toggleFileDiscussionWrappers(diffFile)" > <icon name="comment" /> </gl-button> @@ -282,8 +254,9 @@ export default { <a v-if="diffFile.replaced_view_path" + ref="replacedFileButton" :href="diffFile.replaced_view_path" - class="btn view-file js-view-replaced-file" + class="btn view-file" v-html="viewReplacedFileButtonText" > </a> @@ -292,7 +265,7 @@ export default { ref="expandDiffToFullFileButton" v-gl-tooltip.hover :title="expandDiffToFullFileTitle" - class="expand-file js-expand-file" + class="expand-file" @click="toggleFullDiff(diffFile.file_path)" > <gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline /> @@ -304,7 +277,7 @@ export default { v-gl-tooltip.hover :href="diffFile.view_path" target="blank" - class="view-file js-view-file-button" + class="view-file" :title="viewFileButtonText" > <icon name="doc-text" /> @@ -312,12 +285,13 @@ export default { <a v-if="diffFile.external_url" + ref="externalLink" v-gl-tooltip.hover :href="diffFile.external_url" :title="`View on ${diffFile.formatted_external_url}`" target="_blank" rel="noopener noreferrer" - class="btn btn-file-option js-external-url" + class="btn btn-file-option" > <icon name="external-link" /> </a> diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js new file mode 100644 index 00000000000..ac770c896bd --- /dev/null +++ b/spec/frontend/diffs/components/diff_file_header_spec.js @@ -0,0 +1,472 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; +import EditButton from '~/diffs/components/edit_button.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import diffDiscussionsMockData from '../mock_data/diff_discussions'; +import { truncateSha } from '~/lib/utils/text_utility'; +import { diffViewerModes } from '~/ide/constants'; +import { __, sprintf } from '~/locale'; +import { scrollToElement } from '~/lib/utils/common_utils'; + +jest.mock('~/lib/utils/common_utils'); + +const diffFile = Object.freeze( + Object.assign(diffDiscussionsMockData.diff_file, { + edit_path: 'link:/to/edit/path', + blob: { + id: '848ed9407c6730ff16edb3dd24485a0eea24292a', + path: 'lib/base.js', + name: 'base.js', + mode: '100644', + readable_text: true, + icon: 'file-text-o', + }, + }), +); + +describe('DiffFileHeader component', () => { + let wrapper; + + const diffHasExpandedDiscussionsResultMock = jest.fn(); + const diffHasDiscussionsResultMock = jest.fn(); + const mockStoreConfig = { + state: {}, + modules: { + diffs: { + namespaced: true, + getters: { + diffHasExpandedDiscussions: () => diffHasExpandedDiscussionsResultMock, + diffHasDiscussions: () => diffHasDiscussionsResultMock, + }, + actions: { + toggleFileDiscussions: jest.fn(), + toggleFileDiscussionWrappers: jest.fn(), + toggleFullDiff: jest.fn(), + }, + }, + }, + }; + + afterEach(() => { + [ + diffHasDiscussionsResultMock, + diffHasExpandedDiscussionsResultMock, + ...Object.values(mockStoreConfig.modules.diffs.actions), + ].forEach(mock => mock.mockReset()); + }); + + const findHeader = () => wrapper.find({ ref: 'header' }); + const findTitleLink = () => wrapper.find({ ref: 'titleWrapper' }); + const findExpandButton = () => wrapper.find({ ref: 'expandDiffToFullFileButton' }); + const findFileActions = () => wrapper.find('.file-actions'); + const findModeChangedLine = () => wrapper.find({ ref: 'fileMode' }); + const findLfsLabel = () => wrapper.find('.label-lfs'); + const findToggleDiscussionsButton = () => wrapper.find({ ref: 'toggleDiscussionsButton' }); + const findExternalLink = () => wrapper.find({ ref: 'externalLink' }); + const findReplacedFileButton = () => wrapper.find({ ref: 'replacedFileButton' }); + const findViewFileButton = () => wrapper.find({ ref: 'viewButton' }); + const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' }); + + const findIconByName = iconName => { + const icons = wrapper.findAll(Icon).filter(w => w.props('name') === iconName); + if (icons.length === 0) return icons; + if (icons.length > 1) { + throw new Error(`Multiple icons found for ${iconName}`); + } + return icons.at(0); + }; + + const createComponent = props => { + const localVue = createLocalVue(); + localVue.use(Vuex); + const store = new Vuex.Store(mockStoreConfig); + + wrapper = shallowMount(DiffFileHeader, { + propsData: { + diffFile, + canCurrentUserFork: false, + ...props, + }, + localVue, + store, + sync: false, + }); + }; + + it.each` + visibility | collapsible + ${'visible'} | ${true} + ${'hidden'} | ${false} + `('collapse toggle is $visibility if collapsible is $collapsible', ({ collapsible }) => { + createComponent({ collapsible }); + expect(findCollapseIcon().exists()).toBe(collapsible); + }); + + it.each` + expanded | icon + ${true} | ${'chevron-down'} + ${false} | ${'chevron-right'} + `('collapse icon is $icon if expanded is $expanded', ({ icon, expanded }) => { + createComponent({ expanded, collapsible: true }); + expect(findCollapseIcon().props('name')).toBe(icon); + }); + + it('when header is clicked emits toggleFile', () => { + createComponent(); + findHeader().trigger('click'); + expect(wrapper.emitted().toggleFile).toBeDefined(); + }); + + it('when collapseIcon is clicked emits toggleFile', () => { + createComponent({ collapsible: true }); + findCollapseIcon().vm.$emit('click', new Event('click')); + expect(wrapper.emitted().toggleFile).toBeDefined(); + }); + + it('when other element in header is clicked does not emits toggleFile', () => { + createComponent({ collapsible: true }); + findTitleLink().trigger('click'); + expect(wrapper.emitted().toggleFile).not.toBeDefined(); + }); + + it('displays a copy to clipboard button', () => { + createComponent(); + expect(wrapper.find(ClipboardButton).exists()).toBe(true); + }); + + describe('for submodule', () => { + const submoduleDiffFile = { + ...diffFile, + submodule: true, + submodule_link: 'link://to/submodule', + }; + + it('prefers submodule_tree_url over submodule_link for href', () => { + const submoduleTreeUrl = 'some://tree/url'; + createComponent({ + discussionLink: 'discussionLink', + diffFile: { + ...submoduleDiffFile, + submodule_tree_url: 'some://tree/url', + }, + }); + + expect(findTitleLink().attributes('href')).toBe(submoduleTreeUrl); + }); + + it('uses submodule_link for href if submodule_tree_url does not exists', () => { + const submoduleLink = 'link://to/submodule'; + createComponent({ + discussionLink: 'discussionLink', + diffFile: submoduleDiffFile, + }); + + expect(findTitleLink().attributes('href')).toBe(submoduleLink); + }); + + it('uses file_path + SHA as link text', () => { + createComponent({ + diffFile: submoduleDiffFile, + }); + + expect(findTitleLink().text()).toContain( + `${diffFile.file_path} @ ${truncateSha(diffFile.blob.id)}`, + ); + }); + + it('does not render file actions', () => { + createComponent({ + diffFile: submoduleDiffFile, + addMergeRequestButtons: true, + }); + expect(findFileActions().exists()).toBe(false); + }); + }); + + describe('for any file', () => { + const otherModes = Object.keys(diffViewerModes).filter(m => m !== 'mode_changed'); + + it('when edit button emits showForkMessage event it is re-emitted', () => { + createComponent({ + addMergeRequestButtons: true, + }); + wrapper.find(EditButton).vm.$emit('showForkMessage'); + expect(wrapper.emitted().showForkMessage).toBeDefined(); + }); + + it('for mode_changed file mode displays mode changes', () => { + createComponent({ + diffFile: { + ...diffFile, + a_mode: 'old-mode', + b_mode: 'new-mode', + viewer: { + ...diffFile.viewer, + name: diffViewerModes.mode_changed, + }, + }, + }); + expect(findModeChangedLine().text()).toMatch(/old-mode.+new-mode/); + }); + + it.each(otherModes.map(m => [m]))('for %s file mode does not display mode changes', mode => { + createComponent({ + diffFile: { + ...diffFile, + a_mode: 'old-mode', + b_mode: 'new-mode', + viewer: { + ...diffFile.viewer, + name: diffViewerModes[mode], + }, + }, + }); + expect(findModeChangedLine().exists()).toBeFalsy(); + }); + + it('displays the LFS label for files stored in LFS', () => { + createComponent({ + diffFile: { ...diffFile, stored_externally: true, external_storage: 'lfs' }, + }); + expect(findLfsLabel().exists()).toBe(true); + }); + + it('does not display the LFS label for files stored in repository', () => { + createComponent({ + diffFile: { ...diffFile, stored_externally: false }, + }); + expect(findLfsLabel().exists()).toBe(false); + }); + + it('does not render view replaced file button if no replaced view path is present', () => { + createComponent({ + diffFile: { ...diffFile, replaced_view_path: null }, + }); + expect(findReplacedFileButton().exists()).toBe(false); + }); + + describe('when addMergeRequestButtons is false', () => { + it('does not render file actions', () => { + createComponent({ addMergeRequestButtons: false }); + expect(findFileActions().exists()).toBe(false); + }); + it('should not render edit button', () => { + createComponent({ addMergeRequestButtons: false }); + expect(wrapper.find(EditButton).exists()).toBe(false); + }); + }); + + describe('when addMergeRequestButtons is true', () => { + describe('without discussions', () => { + it('renders a disabled toggle discussions button', () => { + diffHasDiscussionsResultMock.mockReturnValue(false); + createComponent({ addMergeRequestButtons: true }); + expect(findToggleDiscussionsButton().attributes('disabled')).toBe('true'); + }); + }); + + describe('with discussions', () => { + it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => { + diffHasDiscussionsResultMock.mockReturnValue(true); + createComponent({ addMergeRequestButtons: true }); + expect(findToggleDiscussionsButton().attributes('disabled')).toBeFalsy(); + findToggleDiscussionsButton().vm.$emit('click'); + expect( + mockStoreConfig.modules.diffs.actions.toggleFileDiscussionWrappers, + ).toHaveBeenCalledWith(expect.any(Object), diffFile, undefined); + }); + }); + + it('should show edit button', () => { + createComponent({ + addMergeRequestButtons: true, + }); + expect(wrapper.find(EditButton).exists()).toBe(true); + }); + + describe('view on environment button', () => { + it('is displayed when external url is provided', () => { + const externalUrl = 'link://to/external'; + const formattedExternalUrl = 'link://formatted'; + createComponent({ + diffFile: { + ...diffFile, + external_url: externalUrl, + formatted_external_url: formattedExternalUrl, + }, + addMergeRequestButtons: true, + }); + expect(findExternalLink().exists()).toBe(true); + }); + + it('is hidden by default', () => { + createComponent({ addMergeRequestButtons: true }); + expect(findExternalLink().exists()).toBe(false); + }); + }); + + describe('without file blob', () => { + beforeEach(() => { + createComponent({ diffFile: { ...diffFile, blob: false } }); + }); + + it('should not render toggle discussions button', () => { + expect(findToggleDiscussionsButton().exists()).toBe(false); + }); + + it('should not render edit button', () => { + expect(wrapper.find(EditButton).exists()).toBe(false); + }); + }); + describe('with file blob', () => { + it('should render correct file view button', () => { + const viewPath = 'link://view-path'; + createComponent({ + diffFile: { ...diffFile, view_path: viewPath }, + addMergeRequestButtons: true, + }); + expect(findViewFileButton().attributes('href')).toBe(viewPath); + expect(findViewFileButton().attributes('data-original-title')).toEqual( + `View file @ ${diffFile.content_sha.substr(0, 8)}`, + ); + }); + }); + }); + + describe('expand full file button', () => { + describe('when diff is fully expanded', () => { + it('is not rendered', () => { + createComponent({ + diffFile: { + ...diffFile, + is_fully_expanded: true, + }, + }); + expect(findExpandButton().exists()).toBe(false); + }); + }); + describe('when diff is not fully expanded', () => { + const fullyNotExpandedFileProps = { + diffFile: { + ...diffFile, + is_fully_expanded: false, + edit_path: 'link/to/edit/path.txt', + isShowingFullFile: false, + }, + addMergeRequestButtons: true, + }; + + it.each` + iconName | isShowingFullFile + ${'doc-expand'} | ${false} + ${'doc-changes'} | ${true} + `( + 'shows $iconName when isShowingFullFile set to $isShowingFullFile', + ({ iconName, isShowingFullFile }) => { + createComponent({ + ...fullyNotExpandedFileProps, + diffFile: { ...fullyNotExpandedFileProps.diffFile, isShowingFullFile }, + }); + expect(findIconByName(iconName).exists()).toBe(true); + }, + ); + + it('renders expand to full file button if not showing full file already', () => { + createComponent(fullyNotExpandedFileProps); + expect(findExpandButton().exists()).toBe(true); + }); + + it('renders loading icon when loading full file', () => { + createComponent(fullyNotExpandedFileProps); + expect(findExpandButton().exists()).toBe(true); + }); + + it('toggles full diff on click', () => { + createComponent(fullyNotExpandedFileProps); + findExpandButton().vm.$emit('click'); + expect(mockStoreConfig.modules.diffs.actions.toggleFullDiff).toHaveBeenCalled(); + }); + }); + }); + + it('uses discussionPath for link if it is defined', () => { + const discussionPath = 'link://to/discussion'; + createComponent({ + discussionPath, + }); + expect(findTitleLink().attributes('href')).toBe(discussionPath); + }); + + it('uses local anchor for link as last resort', () => { + createComponent(); + expect(findTitleLink().attributes('href')).toMatch(/^#diff-content/); + }); + + describe('when local anchor for link is clicked', () => { + beforeEach(() => { + createComponent(); + }); + + it('scrolls to target', () => { + findTitleLink().trigger('click'); + expect(scrollToElement).toHaveBeenCalled(); + }); + + it('updates anchor in URL', () => { + findTitleLink().trigger('click'); + expect(window.location.href).toMatch(/#diff-content/); + }); + }); + }); + + describe('for new file', () => { + it('displays the path', () => { + createComponent({ diffFile: { ...diffFile, new_file: true } }); + expect(findTitleLink().text()).toBe(diffFile.file_path); + }); + }); + + describe('for deleted file', () => { + it('displays the path', () => { + createComponent({ diffFile: { ...diffFile, deleted_file: true } }); + expect(findTitleLink().text()).toBe( + sprintf(__('%{filePath} deleted'), { filePath: diffFile.file_path }, false), + ); + }); + + it('does not show edit button', () => { + createComponent({ diffFile: { ...diffFile, deleted_file: true } }); + expect(wrapper.find(EditButton).exists()).toBe(false); + }); + }); + + describe('for renamed file', () => { + it('displays old and new path if the file was renamed', () => { + createComponent({ + diffFile: { + ...diffFile, + renamed_file: true, + old_path_html: 'old', + new_path_html: 'new', + }, + }); + expect(findTitleLink().text()).toMatch(/^old.+new/s); + }); + }); + + describe('for replaced file', () => { + it('renders view replaced file button', () => { + const replacedViewPath = 'some/path'; + createComponent({ + diffFile: { + ...diffFile, + replaced_view_path: replacedViewPath, + }, + addMergeRequestButtons: true, + }); + expect(findReplacedFileButton().exists()).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js deleted file mode 100644 index 356e7a8f1fe..00000000000 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ /dev/null @@ -1,713 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import diffsModule from '~/diffs/store/modules'; -import notesModule from '~/notes/stores/modules'; -import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; -import mountComponent, { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import diffDiscussionsMockData from '../mock_data/diff_discussions'; -import { diffViewerModes } from '~/ide/constants'; - -Vue.use(Vuex); - -describe('diff_file_header', () => { - let vm; - let props; - const diffDiscussionMock = diffDiscussionsMockData; - const Component = Vue.extend(DiffFileHeader); - - const store = new Vuex.Store({ - modules: { - diffs: diffsModule(), - notes: notesModule(), - }, - }); - - beforeEach(() => { - const diffFile = diffDiscussionMock.diff_file; - - diffFile.added_lines = 2; - diffFile.removed_lines = 1; - - props = { - diffFile: { ...diffFile }, - canCurrentUserFork: false, - }; - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('computed', () => { - describe('icon', () => { - beforeEach(() => { - props.diffFile.blob.icon = 'file-text-o'; - }); - - it('returns the blob icon for files', () => { - props.diffFile.submodule = false; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.icon).toBe(props.diffFile.blob.icon); - }); - - it('returns the archive icon for submodules', () => { - props.diffFile.submodule = true; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.icon).toBe('archive'); - }); - }); - - describe('titleLink', () => { - beforeEach(() => { - props.discussionPath = 'link://to/discussion'; - Object.assign(props.diffFile, { - submodule_link: 'link://to/submodule', - submodule_tree_url: 'some://tree/url', - }); - }); - - it('returns the discussionPath for files', () => { - props.diffFile.submodule = false; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.titleLink).toBe(props.discussionPath); - }); - - it('returns the submoduleTreeUrl for submodules', () => { - props.diffFile.submodule = true; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.titleLink).toBe(props.diffFile.submodule_tree_url); - }); - - it('returns the submoduleLink for submodules without submoduleTreeUrl', () => { - Object.assign(props.diffFile, { - submodule: true, - submodule_tree_url: null, - }); - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.titleLink).toBe(props.diffFile.submodule_link); - }); - - it('sets the correct path to the discussion', () => { - props.discussionPath = 'link://to/discussion'; - vm = mountComponentWithStore(Component, { props, store }); - const href = vm.$el.querySelector('.js-title-wrapper').getAttribute('href'); - - expect(href).toBe(vm.discussionPath); - }); - }); - - describe('filePath', () => { - beforeEach(() => { - Object.assign(props.diffFile, { - blob: { id: 'b10b1db10b1d' }, - file_path: 'path/to/file', - }); - }); - - it('returns the filePath for files', () => { - props.diffFile.submodule = false; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.filePath).toBe(props.diffFile.file_path); - }); - - it('appends the truncated blob id for submodules', () => { - props.diffFile.submodule = true; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.filePath).toBe( - `${props.diffFile.file_path} @ ${props.diffFile.blob.id.substr(0, 8)}`, - ); - }); - }); - - describe('titleTag', () => { - it('returns a link tag if fileHash is set', () => { - props.diffFile.file_hash = 'some hash'; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.titleTag).toBe('a'); - }); - - it('returns a span tag if fileHash is not set', () => { - props.diffFile.file_hash = null; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.titleTag).toBe('span'); - }); - }); - - describe('isUsingLfs', () => { - beforeEach(() => { - Object.assign(props.diffFile, { - stored_externally: true, - external_storage: 'lfs', - }); - }); - - it('returns true if file is stored in LFS', () => { - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.isUsingLfs).toBe(true); - }); - - it('returns false if file is not stored externally', () => { - props.diffFile.stored_externally = false; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.isUsingLfs).toBe(false); - }); - - it('returns false if file is not stored in LFS', () => { - props.diffFile.external_storage = 'not lfs'; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.isUsingLfs).toBe(false); - }); - }); - - describe('collapseIcon', () => { - it('returns chevron-down if the diff is expanded', () => { - props.expanded = true; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.collapseIcon).toBe('chevron-down'); - }); - - it('returns chevron-right if the diff is collapsed', () => { - props.expanded = false; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.collapseIcon).toBe('chevron-right'); - }); - }); - - describe('viewFileButtonText', () => { - it('contains the truncated content SHA', () => { - const dummySha = 'deebd00f is no SHA'; - props.diffFile.content_sha = dummySha; - - vm = mountComponentWithStore(Component, { props, store }); - - 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.diff_refs.base_sha = dummySha; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.viewReplacedFileButtonText).not.toContain(dummySha); - expect(vm.viewReplacedFileButtonText).toContain(dummySha.substr(0, 8)); - }); - }); - }); - - describe('methods', () => { - describe('handleToggleFile', () => { - beforeEach(() => { - spyOn(vm, '$emit').and.stub(); - }); - - it('emits toggleFile if checkTarget is false', () => { - vm.handleToggleFile(null, false); - - expect(vm.$emit).toHaveBeenCalledWith('toggleFile'); - }); - - it('emits toggleFile if checkTarget is true and event target is header', () => { - vm.handleToggleFile({ 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.handleToggleFile({ target: 'not header' }, true); - - expect(vm.$emit).not.toHaveBeenCalled(); - }); - }); - - describe('handleFileNameClick', () => { - let e; - - beforeEach(() => { - e = { preventDefault: () => {} }; - spyOn(e, 'preventDefault'); - }); - - describe('when file name links to other page', () => { - it('does not call preventDefault if submodule tree url exists', () => { - vm = mountComponent(Component, { - ...props, - diffFile: { ...props.diffFile, submodule_tree_url: 'foobar.com' }, - }); - - vm.handleFileNameClick(e); - - expect(e.preventDefault).not.toHaveBeenCalled(); - }); - - it('does not call preventDefault if submodule_link exists', () => { - vm = mountComponent(Component, { - ...props, - diffFile: { ...props.diffFile, submodule_link: 'foobar.com' }, - }); - vm.handleFileNameClick(e); - - expect(e.preventDefault).not.toHaveBeenCalled(); - }); - - it('does not call preventDefault if discussionPath exists', () => { - vm = mountComponent(Component, { - ...props, - discussionPath: 'Foo bar', - }); - - vm.handleFileNameClick(e); - - expect(e.preventDefault).not.toHaveBeenCalled(); - }); - }); - - describe('scrolling to diff', () => { - let scrollToElement; - let el; - - beforeEach(() => { - el = document.createElement('div'); - spyOn(document, 'querySelector').and.returnValue(el); - scrollToElement = spyOnDependency(DiffFileHeader, 'scrollToElement'); - vm = mountComponent(Component, props); - - vm.handleFileNameClick(e); - }); - - it('calls scrollToElement with file content', () => { - expect(scrollToElement).toHaveBeenCalledWith(el); - }); - - it('element adds the content id to the window location', () => { - expect(window.location.hash).toContain(props.diffFile.file_hash); - }); - - it('calls preventDefault when button does not link to other page', () => { - expect(e.preventDefault).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 = mountComponentWithStore(Component, { props, store }); - - expect(collapseToggle()).not.toBe(null); - }); - - it('is hidden if collapsible is false', () => { - props.collapsible = false; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(collapseToggle()).toBe(null); - }); - }); - - it('displays an file icon in the title', () => { - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('svg.js-file-icon use').getAttribute('xlink:href')).toContain( - 'ruby', - ); - }); - - describe('file paths', () => { - const filePaths = () => vm.$el.querySelectorAll('.file-title-name'); - - it('displays the path of a added file', () => { - props.diffFile.renamed_file = false; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(filePaths()).toHaveLength(1); - expect(filePaths()[0]).toHaveText(props.diffFile.file_path); - }); - - it('displays path for deleted file', () => { - props.diffFile.renamed_file = false; - props.diffFile.deleted_file = true; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(filePaths()).toHaveLength(1); - expect(filePaths()[0]).toHaveText(`${props.diffFile.file_path} deleted`); - }); - - it('displays old and new path if the file was renamed', () => { - props.diffFile.renamed_file = true; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(filePaths()).toHaveLength(2); - expect(filePaths()[0]).toHaveText(props.diffFile.old_path_html); - expect(filePaths()[1]).toHaveText(props.diffFile.new_path_html); - }); - }); - - it('displays a copy to clipboard button', () => { - vm = mountComponentWithStore(Component, { props, store }); - - const button = vm.$el.querySelector('.btn-clipboard'); - - expect(button).not.toBe(null); - expect(button.dataset.clipboardText).toBe('{"text":"CHANGELOG.rb","gfm":"`CHANGELOG.rb`"}'); - }); - - describe('file mode', () => { - it('it displays old and new file mode if it changed', () => { - props.diffFile.viewer.name = diffViewerModes.mode_changed; - - vm = mountComponentWithStore(Component, { props, store }); - - const { fileMode } = vm.$refs; - - expect(fileMode).not.toBe(undefined); - expect(fileMode).toContainText(props.diffFile.a_mode); - expect(fileMode).toContainText(props.diffFile.b_mode); - }); - - it('does not display the file mode if it has not changed', () => { - props.diffFile.viewer.name = diffViewerModes.text; - - vm = mountComponentWithStore(Component, { props, store }); - - 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, { - stored_externally: true, - external_storage: 'lfs', - }); - - vm = mountComponentWithStore(Component, { props, store }); - - expect(lfsLabel()).not.toBe(null); - expect(lfsLabel()).toHaveText('LFS'); - }); - - it('does not display the LFS label for files stored in repository', () => { - props.diffFile.stored_externally = false; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(lfsLabel()).toBe(null); - }); - }); - - describe('edit button', () => { - it('should not render edit button if addMergeRequestButtons is not true', () => { - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null); - }); - - it('should show edit button when file is editable', () => { - props.addMergeRequestButtons = true; - props.diffFile.edit_path = '/'; - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.js-edit-blob')).not.toBe(null); - }); - - it('should not show edit button when file is deleted', () => { - props.addMergeRequestButtons = true; - props.diffFile.deleted_file = true; - props.diffFile.edit_path = '/'; - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null); - }); - }); - - describe('addMergeRequestButtons', () => { - beforeEach(() => { - props.addMergeRequestButtons = true; - props.diffFile.edit_path = ''; - }); - - describe('view on environment button', () => { - const url = 'some.external.url/'; - const title = 'url.title'; - - it('displays link to external url', () => { - props.diffFile.external_url = url; - props.diffFile.formatted_external_url = title; - - vm = mountComponentWithStore(Component, { props, store }); - - 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.external_url = ''; - props.diffFile.formattedExternal_url = title; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).toBe(null); - }); - }); - }); - - describe('handles toggle discussions', () => { - it('renders a disabled button when diff has no discussions', () => { - const propsCopy = Object.assign({}, props); - propsCopy.diffFile.submodule = false; - propsCopy.diffFile.blob = { - id: '848ed9407c6730ff16edb3dd24485a0eea24292a', - path: 'lib/base.js', - name: 'base.js', - mode: '100644', - readable_text: true, - icon: 'file-text-o', - }; - propsCopy.addMergeRequestButtons = true; - propsCopy.diffFile.deleted_file = true; - - vm = mountComponentWithStore(Component, { - props: propsCopy, - store, - }); - - expect( - vm.$el.querySelector('.js-btn-vue-toggle-comments').getAttribute('disabled'), - ).toEqual('disabled'); - }); - - describe('with discussions', () => { - it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => { - const propsCopy = Object.assign({}, props); - propsCopy.diffFile.submodule = false; - propsCopy.diffFile.blob = { - id: '848ed9407c6730ff16edb3dd24485a0eea24292a', - path: 'lib/base.js', - name: 'base.js', - mode: '100644', - readable_text: true, - icon: 'file-text-o', - }; - propsCopy.addMergeRequestButtons = true; - propsCopy.diffFile.deleted_file = true; - - const discussionGetter = () => [ - { - ...diffDiscussionMock, - }, - ]; - const notesModuleMock = notesModule(); - notesModuleMock.getters.discussions = discussionGetter; - vm = mountComponentWithStore(Component, { - props: propsCopy, - store: new Vuex.Store({ - modules: { - diffs: diffsModule(), - notes: notesModuleMock, - }, - }), - }); - - spyOn(vm, 'toggleFileDiscussionWrappers'); - - vm.$el.querySelector('.js-btn-vue-toggle-comments').click(); - - expect(vm.toggleFileDiscussionWrappers).toHaveBeenCalled(); - }); - }); - }); - - describe('file actions', () => { - it('should not render if diff file has a submodule', () => { - props.diffFile.submodule = 'submodule'; - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.file-actions')).toEqual(null); - }); - - it('should not render if add merge request buttons is false', () => { - props.addMergeRequestButtons = false; - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.file-actions')).toEqual(null); - }); - - describe('with add merge request buttons enabled', () => { - beforeEach(() => { - props.addMergeRequestButtons = true; - props.diffFile.edit_path = 'edit-path'; - }); - - const viewReplacedFileButton = () => vm.$el.querySelector('.js-view-replaced-file'); - const viewFileButton = () => vm.$el.querySelector('.js-view-file-button'); - const externalUrl = () => vm.$el.querySelector('.js-external-url'); - - it('should render if add merge request buttons is true and diff file does not have a submodule', () => { - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.file-actions')).not.toEqual(null); - }); - - it('should not render view replaced file button if no replaced view path is present', () => { - vm = mountComponentWithStore(Component, { props, store }); - - expect(viewReplacedFileButton()).toEqual(null); - }); - - it('should render view replaced file button if replaced view path is present', () => { - props.diffFile.replaced_view_path = 'replaced-view-path'; - vm = mountComponentWithStore(Component, { props, store }); - - expect(viewReplacedFileButton()).not.toEqual(null); - expect(viewReplacedFileButton().getAttribute('href')).toBe('replaced-view-path'); - }); - - it('should render correct file view button path', () => { - props.diffFile.view_path = 'view-path'; - vm = mountComponentWithStore(Component, { props, store }); - - expect(viewFileButton().getAttribute('href')).toBe('view-path'); - expect(viewFileButton().getAttribute('data-original-title')).toEqual( - `View file @ ${props.diffFile.content_sha.substr(0, 8)}`, - ); - }); - - it('should not render external url view link if diff file has no external url', () => { - vm = mountComponentWithStore(Component, { props, store }); - - expect(externalUrl()).toEqual(null); - }); - - it('should render external url view link if diff file has external url', () => { - props.diffFile.external_url = 'external_url'; - vm = mountComponentWithStore(Component, { props, store }); - - expect(externalUrl()).not.toEqual(null); - expect(externalUrl().getAttribute('href')).toBe('external_url'); - }); - }); - - describe('without file blob', () => { - beforeEach(() => { - props.diffFile.blob = null; - props.addMergeRequestButtons = true; - vm = mountComponentWithStore(Component, { props, store }); - }); - - it('should not render toggle discussions button', () => { - expect(vm.$el.querySelector('.js-btn-vue-toggle-comments')).toEqual(null); - }); - - it('should not render edit button', () => { - expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null); - }); - }); - }); - }); - - describe('expand full file button', () => { - beforeEach(() => { - props.addMergeRequestButtons = true; - props.diffFile.edit_path = '/'; - }); - - it('does not render button', () => { - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.js-expand-file')).toBe(null); - }); - - it('renders button', () => { - props.diffFile.is_fully_expanded = false; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.js-expand-file')).not.toBe(null); - }); - - it('shows fully expanded text', () => { - props.diffFile.is_fully_expanded = false; - props.diffFile.isShowingFullFile = true; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.ic-doc-changes')).not.toBeNull(); - }); - - it('shows expand text', () => { - props.diffFile.is_fully_expanded = false; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.ic-doc-expand')).not.toBeNull(); - }); - - it('renders loading icon', () => { - props.diffFile.is_fully_expanded = false; - props.diffFile.isLoadingFullFile = true; - - vm = mountComponentWithStore(Component, { props, store }); - - expect(vm.$el.querySelector('.js-expand-file .loading-container')).not.toBe(null); - }); - - it('calls toggleFullDiff on click', () => { - props.diffFile.is_fully_expanded = false; - - vm = mountComponentWithStore(Component, { props, store }); - - spyOn(vm.$store, 'dispatch').and.stub(); - - vm.$el.querySelector('.js-expand-file').click(); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - 'diffs/toggleFullDiff', - props.diffFile.file_path, - ); - }); - }); -}); diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index 5806cb47034..874891fcc6e 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -206,7 +206,7 @@ describe('DiffsStoreActions', () => { position_type: 'text', }, }, - hash: 'diff-content-1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', + hash: 'ABC_123', }, }, ], |