diff options
Diffstat (limited to 'spec/javascripts/diffs/components')
17 files changed, 1324 insertions, 0 deletions
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(); + }); + }); + }); +}); |