diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-20 15:09:17 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-20 15:09:17 +0000 |
commit | 194b499aa8e26df26ff70a1e1ce0396587bd5243 (patch) | |
tree | c873ac9c3096faf4a5da43d6670107461da2a7d7 /spec/frontend/diffs | |
parent | 43b4b3e2d2ddebc0a89b94a8251c162ec5719780 (diff) | |
download | gitlab-ce-194b499aa8e26df26ff70a1e1ce0396587bd5243.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/diffs')
18 files changed, 5113 insertions, 0 deletions
diff --git a/spec/frontend/diffs/components/commit_widget_spec.js b/spec/frontend/diffs/components/commit_widget_spec.js new file mode 100644 index 00000000000..54e7596b726 --- /dev/null +++ b/spec/frontend/diffs/components/commit_widget_spec.js @@ -0,0 +1,19 @@ +import { shallowMount } from '@vue/test-utils'; +import CommitWidget from '~/diffs/components/commit_widget.vue'; +import CommitItem from '~/diffs/components/commit_item.vue'; + +describe('diffs/components/commit_widget', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(CommitWidget, { + propsData: { commit: {} }, + }); + }); + + it('renders commit item', () => { + const commitElement = wrapper.find(CommitItem); + + expect(commitElement.exists()).toBe(true); + }); +}); diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js new file mode 100644 index 00000000000..ba5a4f96204 --- /dev/null +++ b/spec/frontend/diffs/components/diff_discussions_spec.js @@ -0,0 +1,102 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import DiffDiscussions from '~/diffs/components/diff_discussions.vue'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +import NoteableDiscussion from '~/notes/components/noteable_discussion.vue'; +import DiscussionNotes from '~/notes/components/discussion_notes.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import { createStore } from '~/mr_notes/stores'; +import '~/behaviors/markdown/render_gfm'; +import discussionsMockData from '../mock_data/diff_discussions'; + +const localVue = createLocalVue(); + +describe('DiffDiscussions', () => { + let store; + let wrapper; + const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + + const createComponent = props => { + store = createStore(); + wrapper = mount(localVue.extend(DiffDiscussions), { + store, + propsData: { + discussions: getDiscussionsMockData(), + ...props, + }, + localVue, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + it('should have notes list', () => { + createComponent(); + + expect(wrapper.find(NoteableDiscussion).exists()).toBe(true); + expect(wrapper.find(DiscussionNotes).exists()).toBe(true); + expect(wrapper.find(DiscussionNotes).findAll(TimelineEntryItem).length).toBe( + discussionsMockData.notes.length, + ); + }); + }); + + describe('image commenting', () => { + const findDiffNotesToggle = () => wrapper.find('.js-diff-notes-toggle'); + + it('renders collapsible discussion button', () => { + createComponent({ shouldCollapseDiscussions: true }); + const diffNotesToggle = findDiffNotesToggle(); + + expect(diffNotesToggle.exists()).toBe(true); + expect(diffNotesToggle.find(Icon).exists()).toBe(true); + expect(diffNotesToggle.classes('diff-notes-collapse')).toBe(true); + }); + + it('dispatches toggleDiscussion when clicking collapse button', () => { + createComponent({ shouldCollapseDiscussions: true }); + jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(); + const diffNotesToggle = findDiffNotesToggle(); + diffNotesToggle.trigger('click'); + + expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', { + discussionId: discussionsMockData.id, + }); + }); + + it('renders expand button when discussion is collapsed', () => { + const discussions = getDiscussionsMockData(); + discussions[0].expanded = false; + createComponent({ discussions, shouldCollapseDiscussions: true }); + const diffNotesToggle = findDiffNotesToggle(); + + expect(diffNotesToggle.text().trim()).toBe('1'); + expect(diffNotesToggle.classes()).toEqual( + expect.arrayContaining(['btn-transparent', 'badge', 'badge-pill']), + ); + }); + + it('hides discussion when collapsed', () => { + const discussions = getDiscussionsMockData(); + discussions[0].expanded = false; + createComponent({ discussions, shouldCollapseDiscussions: true }); + + expect(wrapper.find(NoteableDiscussion).isVisible()).toBe(false); + }); + + it('renders badge on avatar', () => { + createComponent({ renderAvatarBadge: true }); + const noteableDiscussion = wrapper.find(NoteableDiscussion); + + expect(noteableDiscussion.find('.badge-pill').exists()).toBe(true); + expect( + noteableDiscussion + .find('.badge-pill') + .text() + .trim(), + ).toBe('1'); + }); + }); +}); diff --git a/spec/frontend/diffs/components/diff_expansion_cell_spec.js b/spec/frontend/diffs/components/diff_expansion_cell_spec.js new file mode 100644 index 00000000000..31c6a4d5b60 --- /dev/null +++ b/spec/frontend/diffs/components/diff_expansion_cell_spec.js @@ -0,0 +1,229 @@ +import Vue from 'vue'; +import { cloneDeep } from 'lodash'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { createStore } from '~/mr_notes/stores'; +import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue'; +import { getPreviousLineIndex } from '~/diffs/store/utils'; +import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants'; +import diffFileMockData from '../mock_data/diff_file'; + +const EXPAND_UP_CLASS = '.js-unfold'; +const EXPAND_DOWN_CLASS = '.js-unfold-down'; +const EXPAND_ALL_CLASS = '.js-unfold-all'; +const LINE_TO_USE = 5; +const lineSources = { + [INLINE_DIFF_VIEW_TYPE]: 'highlighted_diff_lines', + [PARALLEL_DIFF_VIEW_TYPE]: 'parallel_diff_lines', +}; +const lineHandlers = { + [INLINE_DIFF_VIEW_TYPE]: line => line, + [PARALLEL_DIFF_VIEW_TYPE]: line => line.right || line.left, +}; + +function makeLoadMoreLinesPayload({ + sinceLine, + toLine, + oldLineNumber, + diffViewType, + fileHash, + nextLineNumbers = {}, + unfold = false, + bottom = false, + isExpandDown = false, +}) { + return { + endpoint: 'contextLinesPath', + params: { + since: sinceLine, + to: toLine, + offset: toLine + 1 - oldLineNumber, + view: diffViewType, + unfold, + bottom, + }, + lineNumbers: { + oldLineNumber, + newLineNumber: toLine + 1, + }, + nextLineNumbers, + fileHash, + isExpandDown, + }; +} + +function getLine(file, type, index) { + const source = lineSources[type]; + const handler = lineHandlers[type]; + + return handler(file[source][index]); +} + +describe('DiffExpansionCell', () => { + let mockFile; + let mockLine; + let store; + let vm; + + beforeEach(() => { + mockFile = cloneDeep(diffFileMockData); + mockLine = getLine(mockFile, INLINE_DIFF_VIEW_TYPE, LINE_TO_USE); + store = createStore(); + store.state.diffs.diffFiles = [mockFile]; + jest.spyOn(store, 'dispatch').mockReturnValue(Promise.resolve()); + }); + + const createComponent = (options = {}) => { + const cmp = Vue.extend(DiffExpansionCell); + const defaults = { + fileHash: mockFile.file_hash, + contextLinesPath: 'contextLinesPath', + line: mockLine, + isTop: false, + isBottom: false, + }; + const props = Object.assign({}, defaults, options); + + vm = createComponentWithStore(cmp, store, props).$mount(); + }; + + const findExpandUp = () => vm.$el.querySelector(EXPAND_UP_CLASS); + const findExpandDown = () => vm.$el.querySelector(EXPAND_DOWN_CLASS); + const findExpandAll = () => vm.$el.querySelector(EXPAND_ALL_CLASS); + + describe('top row', () => { + it('should have "expand up" and "show all" option', () => { + createComponent({ + isTop: true, + }); + + expect(findExpandUp()).not.toBe(null); + expect(findExpandDown()).toBe(null); + expect(findExpandAll()).not.toBe(null); + }); + }); + + describe('middle row', () => { + it('should have "expand down", "show all", "expand up" option', () => { + createComponent(); + + expect(findExpandUp()).not.toBe(null); + expect(findExpandDown()).not.toBe(null); + expect(findExpandAll()).not.toBe(null); + }); + }); + + describe('bottom row', () => { + it('should have "expand down" and "show all" option', () => { + createComponent({ + isBottom: true, + }); + + expect(findExpandUp()).toBe(null); + expect(findExpandDown()).not.toBe(null); + expect(findExpandAll()).not.toBe(null); + }); + }); + + describe('any row', () => { + [ + { diffViewType: INLINE_DIFF_VIEW_TYPE, file: { parallel_diff_lines: [] } }, + { diffViewType: PARALLEL_DIFF_VIEW_TYPE, file: { highlighted_diff_lines: [] } }, + ].forEach(({ diffViewType, file }) => { + describe(`with diffViewType (${diffViewType})`, () => { + beforeEach(() => { + mockLine = getLine(mockFile, diffViewType, LINE_TO_USE); + store.state.diffs.diffFiles = [{ ...mockFile, ...file }]; + store.state.diffs.diffViewType = diffViewType; + }); + + it('does not initially dispatch anything', () => { + expect(store.dispatch).not.toHaveBeenCalled(); + }); + + it('on expand all clicked, dispatch loadMoreLines', () => { + const oldLineNumber = mockLine.meta_data.old_pos; + const newLineNumber = mockLine.meta_data.new_pos; + const previousIndex = getPreviousLineIndex(diffViewType, mockFile, { + oldLineNumber, + newLineNumber, + }); + + createComponent(); + + findExpandAll().click(); + + expect(store.dispatch).toHaveBeenCalledWith( + 'diffs/loadMoreLines', + makeLoadMoreLinesPayload({ + fileHash: mockFile.file_hash, + toLine: newLineNumber - 1, + sinceLine: previousIndex, + oldLineNumber, + diffViewType, + }), + ); + }); + + it('on expand up clicked, dispatch loadMoreLines', () => { + mockLine.meta_data.old_pos = 200; + mockLine.meta_data.new_pos = 200; + + const oldLineNumber = mockLine.meta_data.old_pos; + const newLineNumber = mockLine.meta_data.new_pos; + + createComponent(); + + findExpandUp().click(); + + expect(store.dispatch).toHaveBeenCalledWith( + 'diffs/loadMoreLines', + makeLoadMoreLinesPayload({ + fileHash: mockFile.file_hash, + toLine: newLineNumber - 1, + sinceLine: 179, + oldLineNumber, + diffViewType, + unfold: true, + }), + ); + }); + + it('on expand down clicked, dispatch loadMoreLines', () => { + mockFile[lineSources[diffViewType]][LINE_TO_USE + 1] = cloneDeep( + mockFile[lineSources[diffViewType]][LINE_TO_USE], + ); + const nextLine = getLine(mockFile, diffViewType, LINE_TO_USE + 1); + + nextLine.meta_data.old_pos = 300; + nextLine.meta_data.new_pos = 300; + mockLine.meta_data.old_pos = 200; + mockLine.meta_data.new_pos = 200; + + createComponent(); + + findExpandDown().click(); + + expect(store.dispatch).toHaveBeenCalledWith('diffs/loadMoreLines', { + endpoint: 'contextLinesPath', + params: { + since: 1, + to: 21, // the load amount, plus 1 line + offset: 0, + view: diffViewType, + unfold: true, + bottom: true, + }, + lineNumbers: { + // when expanding down, these are based on the previous line, 0, in this case + oldLineNumber: 0, + newLineNumber: 0, + }, + nextLineNumbers: { old_line: 200, new_line: 200 }, + fileHash: mockFile.file_hash, + isExpandDown: true, + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js new file mode 100644 index 00000000000..d0ba71fce47 --- /dev/null +++ b/spec/frontend/diffs/components/diff_file_spec.js @@ -0,0 +1,259 @@ +import Vue from 'vue'; +import { createStore } from 'ee_else_ce/mr_notes/stores'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { mockTracking, triggerEvent } from 'helpers/tracking_helper'; +import DiffFileComponent from '~/diffs/components/diff_file.vue'; +import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; +import diffFileMockDataReadable from '../mock_data/diff_file'; +import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable'; + +describe('DiffFile', () => { + let vm; + let trackingSpy; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { + file: JSON.parse(JSON.stringify(diffFileMockDataReadable)), + canCurrentUserFork: false, + }).$mount(); + trackingSpy = mockTracking('_category_', vm.$el, jest.spyOn); + }); + + afterEach(() => { + vm.$destroy(); + }); + + const findDiffContent = () => vm.$el.querySelector('.diff-content'); + const isVisible = el => el.style.display !== 'none'; + + describe('template', () => { + it('should render component with file header, file content components', done => { + const el = vm.$el; + const { file_hash, file_path } = vm.file; + + expect(el.id).toEqual(file_hash); + 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('.btn-clipboard')).toBeDefined(); + expect(el.querySelector('.file-title-name').innerText.indexOf(file_path)).toBeGreaterThan(-1); + expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); + + vm.file.renderIt = true; + + vm.$nextTick() + .then(() => { + expect(el.querySelectorAll('.line_content').length).toBe(5); + expect(el.querySelectorAll('.js-line-expansion-content').length).toBe(1); + triggerEvent('.btn-clipboard'); + }) + .then(done) + .catch(done.fail); + }); + + it('should track a click event on copy to clip board button', done => { + const el = vm.$el; + + expect(el.querySelector('.btn-clipboard')).toBeDefined(); + vm.file.renderIt = true; + vm.$nextTick() + .then(() => { + triggerEvent('.btn-clipboard'); + + expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_copy_file_button', { + label: 'diff_copy_file_path_button', + property: 'diff_copy_file', + }); + }) + .then(done) + .catch(done.fail); + }); + + describe('collapsed', () => { + it('should not have file content', done => { + expect(isVisible(findDiffContent())).toBe(true); + expect(vm.isCollapsed).toEqual(false); + vm.isCollapsed = true; + vm.file.renderIt = true; + + vm.$nextTick(() => { + expect(isVisible(findDiffContent())).toBe(false); + + done(); + }); + }); + + it('should have collapsed text and link', done => { + vm.renderIt = true; + vm.isCollapsed = 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 collapsed text and link even before rendered', done => { + vm.renderIt = false; + vm.isCollapsed = 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 be collapsable for unreadable files', done => { + vm.$destroy(); + vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { + file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)), + canCurrentUserFork: false, + }).$mount(); + + vm.renderIt = false; + vm.isCollapsed = 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 be collapsed for renamed files', done => { + vm.renderIt = true; + vm.isCollapsed = false; + vm.file.highlighted_diff_lines = null; + vm.file.viewer.name = diffViewerModes.renamed; + + vm.$nextTick(() => { + expect(vm.$el.innerText).not.toContain('This diff is collapsed'); + + done(); + }); + }); + + it('should be collapsed for mode changed files', done => { + vm.renderIt = true; + vm.isCollapsed = false; + vm.file.highlighted_diff_lines = null; + vm.file.viewer.name = diffViewerModes.mode_changed; + + vm.$nextTick(() => { + expect(vm.$el.innerText).not.toContain('This diff is collapsed'); + + done(); + }); + }); + + it('should have loading icon while loading a collapsed diffs', done => { + vm.isCollapsed = true; + vm.isLoadingCollapsedDiff = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('.diff-content.loading').length).toEqual(1); + + done(); + }); + }); + + it('should update store state', done => { + jest.spyOn(vm.$store, 'dispatch').mockImplementation(() => {}); + + vm.isCollapsed = true; + + vm.$nextTick(() => { + expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/setFileCollapsed', { + filePath: vm.file.file_path, + collapsed: true, + }); + + done(); + }); + }); + + it('updates local state when changing file state', done => { + vm.file.viewer.collapsed = true; + + vm.$nextTick(() => { + expect(vm.isCollapsed).toBe(true); + + done(); + }); + }); + }); + }); + + describe('too large diff', () => { + it('should have too large warning and blob link', done => { + const BLOB_LINK = '/file/view/path'; + vm.file.viewer.error = diffViewerErrors.too_large; + vm.file.viewer.error_message = + 'This source diff could not be displayed because it is too large'; + vm.file.view_path = BLOB_LINK; + vm.file.renderIt = true; + + vm.$nextTick(() => { + expect(vm.$el.innerText).toContain( + 'This source diff could not be displayed because it is too large', + ); + + done(); + }); + }); + }); + + describe('watch collapsed', () => { + it('calls handleLoadCollapsedDiff if collapsed changed & file has no lines', done => { + jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {}); + + vm.file.highlighted_diff_lines = undefined; + vm.file.parallel_diff_lines = []; + vm.isCollapsed = true; + + vm.$nextTick() + .then(() => { + vm.isCollapsed = false; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.handleLoadCollapsedDiff).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('does not call handleLoadCollapsedDiff if collapsed changed & file is unreadable', done => { + vm.$destroy(); + vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { + file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)), + canCurrentUserFork: false, + }).$mount(); + + jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {}); + + vm.file.highlighted_diff_lines = undefined; + vm.file.parallel_diff_lines = []; + vm.isCollapsed = true; + + vm.$nextTick() + .then(() => { + vm.isCollapsed = false; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.handleLoadCollapsedDiff).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js new file mode 100644 index 00000000000..9b032d10fdc --- /dev/null +++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js @@ -0,0 +1,108 @@ +import { shallowMount } from '@vue/test-utils'; +import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue'; +import NoteForm from '~/notes/components/note_form.vue'; +import { createStore } from '~/mr_notes/stores'; +import diffFileMockData from '../mock_data/diff_file'; +import { noteableDataMock } from '../../notes/mock_data'; + +describe('DiffLineNoteForm', () => { + let wrapper; + let diffFile; + let diffLines; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + + beforeEach(() => { + diffFile = getDiffFileMock(); + diffLines = diffFile.highlighted_diff_lines; + const store = createStore(); + store.state.notes.userData.id = 1; + store.state.notes.noteableData = noteableDataMock; + + wrapper = shallowMount(DiffLineNoteForm, { + store, + propsData: { + diffFileHash: diffFile.file_hash, + diffLines, + line: diffLines[0], + noteTargetLine: diffLines[0], + }, + }); + }); + + describe('methods', () => { + describe('handleCancelCommentForm', () => { + it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => { + jest.spyOn(window, 'confirm').mockReturnValue(false); + + wrapper.vm.handleCancelCommentForm(true, true); + + expect(window.confirm).toHaveBeenCalled(); + }); + + it('should ask for confirmation when one of the params false', () => { + jest.spyOn(window, 'confirm').mockReturnValue(false); + + wrapper.vm.handleCancelCommentForm(true, false); + + expect(window.confirm).not.toHaveBeenCalled(); + + wrapper.vm.handleCancelCommentForm(false, true); + + expect(window.confirm).not.toHaveBeenCalled(); + }); + + it('should call cancelCommentForm with lineCode', done => { + jest.spyOn(window, 'confirm').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'cancelCommentForm').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'resetAutoSave').mockImplementation(() => {}); + wrapper.vm.handleCancelCommentForm(); + + expect(window.confirm).not.toHaveBeenCalled(); + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.cancelCommentForm).toHaveBeenCalledWith({ + lineCode: diffLines[0].line_code, + fileHash: wrapper.vm.diffFileHash, + }); + + expect(wrapper.vm.resetAutoSave).toHaveBeenCalled(); + + done(); + }); + }); + }); + + describe('saveNoteForm', () => { + it('should call saveNote action with proper params', done => { + const saveDiffDiscussionSpy = jest + .spyOn(wrapper.vm, 'saveDiffDiscussion') + .mockReturnValue(Promise.resolve()); + + wrapper.vm + .handleSaveNote('note body') + .then(() => { + expect(saveDiffDiscussionSpy).toHaveBeenCalledWith({ + note: 'note body', + formData: wrapper.vm.formData, + }); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('mounted', () => { + it('should init autosave', () => { + const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1'; + + expect(wrapper.vm.autosave).toBeDefined(); + expect(wrapper.vm.autosave.key).toEqual(key); + }); + }); + + describe('template', () => { + it('should have note form', () => { + expect(wrapper.find(NoteForm).exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/diffs/components/file_row_stats_spec.js b/spec/frontend/diffs/components/file_row_stats_spec.js new file mode 100644 index 00000000000..34d85ba10b0 --- /dev/null +++ b/spec/frontend/diffs/components/file_row_stats_spec.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import FileRowStats from '~/diffs/components/file_row_stats.vue'; + +describe('Diff file row stats', () => { + let Component; + let vm; + + beforeAll(() => { + Component = Vue.extend(FileRowStats); + }); + + beforeEach(() => { + vm = mountComponent(Component, { + file: { + addedLines: 20, + removedLines: 10, + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders added lines count', () => { + expect(vm.$el.querySelector('.cgreen').textContent).toContain('+20'); + }); + + it('renders removed lines count', () => { + expect(vm.$el.querySelector('.cred').textContent).toContain('-10'); + }); +}); diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js new file mode 100644 index 00000000000..accf0a972d0 --- /dev/null +++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js @@ -0,0 +1,140 @@ +import { shallowMount } from '@vue/test-utils'; +import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue'; +import { createStore } from '~/mr_notes/stores'; +import { imageDiffDiscussions } from '../mock_data/diff_discussions'; +import Icon from '~/vue_shared/components/icon.vue'; + +describe('Diffs image diff overlay component', () => { + const dimensions = { + width: 100, + height: 200, + }; + let wrapper; + let dispatch; + const getAllImageBadges = () => wrapper.findAll('.js-image-badge'); + + function createComponent(props = {}, extendStore = () => {}) { + const store = createStore(); + + extendStore(store); + dispatch = jest.spyOn(store, 'dispatch').mockImplementation(); + + wrapper = shallowMount(ImageDiffOverlay, { + store, + propsData: { + discussions: [...imageDiffDiscussions], + fileHash: 'ABC', + ...props, + }, + methods: { + getImageDimensions: jest.fn().mockReturnValue(dimensions), + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders comment badges', () => { + createComponent(); + + expect(getAllImageBadges()).toHaveLength(2); + }); + + it('renders index of discussion in badge', () => { + createComponent(); + const imageBadges = getAllImageBadges(); + + expect( + imageBadges + .at(0) + .text() + .trim(), + ).toBe('1'); + expect( + imageBadges + .at(1) + .text() + .trim(), + ).toBe('2'); + }); + + it('renders icon when showCommentIcon is true', () => { + createComponent({ showCommentIcon: true }); + + expect(wrapper.find(Icon).exists()).toBe(true); + }); + + it('sets badge comment positions', () => { + createComponent(); + const imageBadges = getAllImageBadges(); + + expect(imageBadges.at(0).attributes('style')).toBe('left: 10px; top: 10px;'); + expect(imageBadges.at(1).attributes('style')).toBe('left: 5px; top: 5px;'); + }); + + it('renders single badge for discussion object', () => { + createComponent({ + discussions: { + ...imageDiffDiscussions[0], + }, + }); + + expect(getAllImageBadges()).toHaveLength(1); + }); + + it('dispatches openDiffFileCommentForm when clicking overlay', () => { + createComponent({ canComment: true }); + wrapper.find('.js-add-image-diff-note-button').trigger('click', { offsetX: 0, offsetY: 0 }); + + expect(dispatch).toHaveBeenCalledWith('diffs/openDiffFileCommentForm', { + fileHash: 'ABC', + x: 0, + y: 0, + width: 100, + height: 200, + }); + }); + + describe('toggle discussion', () => { + const getImageBadge = () => wrapper.find('.js-image-badge'); + + it('disables buttons when shouldToggleDiscussion is false', () => { + createComponent({ shouldToggleDiscussion: false }); + + expect(getImageBadge().attributes('disabled')).toEqual('disabled'); + }); + + it('dispatches toggleDiscussion when clicking image badge', () => { + createComponent(); + getImageBadge().trigger('click'); + + expect(dispatch).toHaveBeenCalledWith('toggleDiscussion', { + discussionId: '1', + }); + }); + }); + + describe('comment form', () => { + const getCommentIndicator = () => wrapper.find('.comment-indicator'); + beforeEach(() => { + createComponent({}, store => { + store.state.diffs.commentForms.push({ + fileHash: 'ABC', + x: 20, + y: 10, + }); + }); + }); + + it('renders comment form badge', () => { + expect(getCommentIndicator().exists()).toBe(true); + }); + + it('sets comment form badge position', () => { + expect(getCommentIndicator().attributes('style')).toBe('left: 20px; top: 10px;'); + }); + }); +}); diff --git a/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js b/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js new file mode 100644 index 00000000000..f423c3b111e --- /dev/null +++ b/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { createStore } from '~/mr_notes/stores'; +import InlineDiffExpansionRow from '~/diffs/components/inline_diff_expansion_row.vue'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('InlineDiffExpansionRow', () => { + const matchLine = diffFileMockData.highlighted_diff_lines[5]; + + const createComponent = (options = {}) => { + const cmp = Vue.extend(InlineDiffExpansionRow); + const defaults = { + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + line: matchLine, + isTop: false, + isBottom: false, + }; + const props = Object.assign({}, defaults, options); + + return createComponentWithStore(cmp, createStore(), props).$mount(); + }; + + describe('template', () => { + it('should render expansion row for match lines', () => { + const vm = createComponent(); + + expect(vm.$el.classList.contains('line_expansion')).toBe(true); + }); + }); +}); diff --git a/spec/frontend/diffs/components/inline_diff_table_row_spec.js b/spec/frontend/diffs/components/inline_diff_table_row_spec.js new file mode 100644 index 00000000000..66349727b11 --- /dev/null +++ b/spec/frontend/diffs/components/inline_diff_table_row_spec.js @@ -0,0 +1,103 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { createStore } from '~/mr_notes/stores'; +import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('InlineDiffTableRow', () => { + let vm; + const thisLine = diffFileMockData.highlighted_diff_lines[0]; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(InlineDiffTableRow), createStore(), { + line: thisLine, + fileHash: diffFileMockData.file_hash, + filePath: diffFileMockData.file_path, + contextLinesPath: 'contextLinesPath', + isHighlighted: false, + }).$mount(); + }); + + it('does not add hll class to line content when line does not match highlighted row', done => { + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.line_content').classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('adds hll class to lineContent when line is the highlighted row', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.highlightedRow = thisLine.line_code; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.line_content').classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + describe('sets coverage title and class', () => { + it('for lines with coverage', done => { + vm.$nextTick() + .then(() => { + const name = diffFileMockData.file_path; + const line = thisLine.new_line; + + vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } }; + + return vm.$nextTick(); + }) + .then(() => { + const coverage = vm.$el.querySelector('.line-coverage'); + + expect(coverage.title).toContain('Test coverage: 5 hits'); + expect(coverage.classList).toContain('coverage'); + }) + .then(done) + .catch(done.fail); + }); + + it('for lines without coverage', done => { + vm.$nextTick() + .then(() => { + const name = diffFileMockData.file_path; + const line = thisLine.new_line; + + vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } }; + + return vm.$nextTick(); + }) + .then(() => { + const coverage = vm.$el.querySelector('.line-coverage'); + + expect(coverage.title).toContain('No test coverage'); + expect(coverage.classList).toContain('no-coverage'); + }) + .then(done) + .catch(done.fail); + }); + + it('for unknown lines', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.coverageFiles = {}; + + return vm.$nextTick(); + }) + .then(() => { + const coverage = vm.$el.querySelector('.line-coverage'); + + expect(coverage.title).not.toContain('Coverage'); + expect(coverage.classList).not.toContain('coverage'); + expect(coverage.classList).not.toContain('no-coverage'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/frontend/diffs/components/inline_diff_view_spec.js b/spec/frontend/diffs/components/inline_diff_view_spec.js new file mode 100644 index 00000000000..a63c13fb271 --- /dev/null +++ b/spec/frontend/diffs/components/inline_diff_view_spec.js @@ -0,0 +1,54 @@ +import Vue from 'vue'; +import '~/behaviors/markdown/render_gfm'; +import { createStore } from 'ee_else_ce/mr_notes/stores'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import InlineDiffView from '~/diffs/components/inline_diff_view.vue'; +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)]; + const notesLength = getDiscussionsMockData()[0].notes.length; + + beforeEach(done => { + const diffFile = getDiffFileMock(); + + const store = createStore(); + + store.dispatch('diffs/setInlineDiffViewType'); + component = createComponentWithStore(Vue.extend(InlineDiffView), store, { + diffFile, + diffLines: diffFile.highlighted_diff_lines, + }).$mount(); + + Vue.nextTick(done); + }); + + describe('template', () => { + it('should have rendered diff lines', () => { + const el = component.$el; + + expect(el.querySelectorAll('tr.line_holder').length).toEqual(5); + expect(el.querySelectorAll('tr.line_holder.new').length).toEqual(2); + expect(el.querySelectorAll('tr.line_expansion.match').length).toEqual(1); + expect(el.textContent.indexOf('Bad dates')).toBeGreaterThan(-1); + }); + + it('should render discussions', done => { + const el = component.$el; + component.diffLines[1].discussions = getDiscussionsMockData(); + component.diffLines[1].discussionsExpanded = true; + + Vue.nextTick(() => { + expect(el.querySelectorAll('.notes_holder').length).toEqual(1); + expect(el.querySelectorAll('.notes_holder .note').length).toEqual(notesLength + 1); + expect(el.innerText.indexOf('comment 5')).toBeGreaterThan(-1); + component.$store.dispatch('setInitialNotes', []); + + done(); + }); + }); + }); +}); diff --git a/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js b/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js new file mode 100644 index 00000000000..15b2a824697 --- /dev/null +++ b/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { createStore } from '~/mr_notes/stores'; +import ParallelDiffExpansionRow from '~/diffs/components/parallel_diff_expansion_row.vue'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('ParallelDiffExpansionRow', () => { + const matchLine = diffFileMockData.highlighted_diff_lines[5]; + + const createComponent = (options = {}) => { + const cmp = Vue.extend(ParallelDiffExpansionRow); + const defaults = { + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + line: matchLine, + isTop: false, + isBottom: false, + }; + const props = Object.assign({}, defaults, options); + + return createComponentWithStore(cmp, createStore(), props).$mount(); + }; + + describe('template', () => { + it('should render expansion row for match lines', () => { + const vm = createComponent(); + + expect(vm.$el.classList.contains('line_expansion')).toBe(true); + }); + }); +}); diff --git a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js new file mode 100644 index 00000000000..6b92d448cf5 --- /dev/null +++ b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js @@ -0,0 +1,147 @@ +import Vue from 'vue'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { createStore } from '~/mr_notes/stores'; +import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('ParallelDiffTableRow', () => { + describe('when one side is empty', () => { + let vm; + const thisLine = diffFileMockData.parallel_diff_lines[0]; + const rightLine = diffFileMockData.parallel_diff_lines[0].right; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), { + line: thisLine, + fileHash: diffFileMockData.file_hash, + filePath: diffFileMockData.file_path, + contextLinesPath: 'contextLinesPath', + isHighlighted: false, + }).$mount(); + }); + + it('does not highlight non empty line content when line does not match highlighted row', done => { + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('highlights nonempty line content when line is the highlighted row', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.highlightedRow = rightLine.line_code; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when both sides have content', () => { + let vm; + const thisLine = diffFileMockData.parallel_diff_lines[2]; + const rightLine = diffFileMockData.parallel_diff_lines[2].right; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), { + line: thisLine, + fileHash: diffFileMockData.file_hash, + filePath: diffFileMockData.file_path, + contextLinesPath: 'contextLinesPath', + isHighlighted: false, + }).$mount(); + }); + + it('does not highlight either line when line does not match highlighted row', done => { + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll'); + expect(vm.$el.querySelector('.line_content.left-side').classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('adds hll class to lineContent when line is the highlighted row', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.highlightedRow = rightLine.line_code; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll'); + expect(vm.$el.querySelector('.line_content.left-side').classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + describe('sets coverage title and class', () => { + it('for lines with coverage', done => { + vm.$nextTick() + .then(() => { + const name = diffFileMockData.file_path; + const line = rightLine.new_line; + + vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } }; + + return vm.$nextTick(); + }) + .then(() => { + const coverage = vm.$el.querySelector('.line-coverage.right-side'); + + expect(coverage.title).toContain('Test coverage: 5 hits'); + expect(coverage.classList).toContain('coverage'); + }) + .then(done) + .catch(done.fail); + }); + + it('for lines without coverage', done => { + vm.$nextTick() + .then(() => { + const name = diffFileMockData.file_path; + const line = rightLine.new_line; + + vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } }; + + return vm.$nextTick(); + }) + .then(() => { + const coverage = vm.$el.querySelector('.line-coverage.right-side'); + + expect(coverage.title).toContain('No test coverage'); + expect(coverage.classList).toContain('no-coverage'); + }) + .then(done) + .catch(done.fail); + }); + + it('for unknown lines', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.coverageFiles = {}; + + return vm.$nextTick(); + }) + .then(() => { + const coverage = vm.$el.querySelector('.line-coverage.right-side'); + + expect(coverage.title).not.toContain('Coverage'); + expect(coverage.classList).not.toContain('coverage'); + expect(coverage.classList).not.toContain('no-coverage'); + }) + .then(done) + .catch(done.fail); + }); + }); + }); +}); diff --git a/spec/frontend/diffs/components/parallel_diff_view_spec.js b/spec/frontend/diffs/components/parallel_diff_view_spec.js new file mode 100644 index 00000000000..0eefbc7ec08 --- /dev/null +++ b/spec/frontend/diffs/components/parallel_diff_view_spec.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import { createStore } from 'ee_else_ce/mr_notes/stores'; +import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue'; +import * as constants from '~/diffs/constants'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('ParallelDiffView', () => { + let component; + const getDiffFileMock = () => Object.assign({}, diffFileMockData); + + beforeEach(() => { + const diffFile = getDiffFileMock(); + + component = createComponentWithStore(Vue.extend(ParallelDiffView), createStore(), { + diffFile, + diffLines: diffFile.parallel_diff_lines, + }).$mount(); + }); + + afterEach(() => { + component.$destroy(); + }); + + describe('assigned', () => { + describe('diffLines', () => { + it('should normalize lines for empty cells', () => { + expect(component.diffLines[0].left.type).toEqual(constants.EMPTY_CELL_TYPE); + expect(component.diffLines[1].left.type).toEqual(constants.EMPTY_CELL_TYPE); + }); + }); + }); +}); diff --git a/spec/frontend/diffs/mock_data/diff_file_unreadable.js b/spec/frontend/diffs/mock_data/diff_file_unreadable.js new file mode 100644 index 00000000000..8c2df45988e --- /dev/null +++ b/spec/frontend/diffs/mock_data/diff_file_unreadable.js @@ -0,0 +1,244 @@ +export default { + submodule: false, + submodule_link: null, + blob: { + id: '9e10516ca50788acf18c518a231914a21e5f16f7', + path: 'CHANGELOG', + name: 'CHANGELOG', + mode: '100644', + readable_text: false, + 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, + viewer: { + name: 'text', + error: null, + collapsed: false, + }, + added_lines: 0, + 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', + new_path_html: 'CHANGELOG', + edit_path: '/gitlab-org/gitlab-test/edit/spooky-stuff/CHANGELOG', + view_path: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG', + replaced_view_path: null, + collapsed: false, + renderIt: false, + too_large: false, + 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, + discussions: [], + 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, + discussions: [], + 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, + discussions: [], + 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, + discussions: [], + 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, + discussions: [], + 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, + discussions: [], + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + ], + parallel_diff_lines: [ + { + left: { + type: 'empty-cell', + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + old_line: null, + new_line: 1, + discussions: [], + 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: { + type: 'empty-cell', + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + old_line: null, + new_line: 2, + discussions: [], + 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, + discussions: [], + 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, + discussions: [], + 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, + discussions: [], + 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, + discussions: [], + 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, + discussions: [], + 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, + discussions: [], + 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, + discussions: [], + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + right: { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + discussions: [], + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + }, + ], + discussions: [], + renderingLines: false, +}; diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js new file mode 100644 index 00000000000..8a1c3e56e5a --- /dev/null +++ b/spec/frontend/diffs/store/actions_spec.js @@ -0,0 +1,1349 @@ +import MockAdapter from 'axios-mock-adapter'; +import Cookies from 'js-cookie'; +import mockDiffFile from 'jest/diffs/mock_data/diff_file'; +import { + DIFF_VIEW_COOKIE_NAME, + INLINE_DIFF_VIEW_TYPE, + PARALLEL_DIFF_VIEW_TYPE, + DIFFS_PER_PAGE, +} from '~/diffs/constants'; +import { + setBaseConfig, + fetchDiffFiles, + fetchDiffFilesBatch, + fetchDiffFilesMeta, + fetchCoverageFiles, + assignDiscussionsToDiff, + removeDiscussionsFromDiff, + startRenderDiffsQueue, + setInlineDiffViewType, + setParallelDiffViewType, + showCommentForm, + cancelCommentForm, + loadMoreLines, + scrollToLineIfNeededInline, + scrollToLineIfNeededParallel, + loadCollapsedDiff, + expandAllFiles, + toggleFileDiscussions, + saveDiffDiscussion, + setHighlightedRow, + toggleTreeOpen, + scrollToFile, + toggleShowTreeList, + renderFileForDiscussionId, + setRenderTreeList, + setShowWhitespace, + setRenderIt, + requestFullDiff, + receiveFullDiffSucess, + receiveFullDiffError, + fetchFullDiff, + toggleFullDiff, + setFileCollapsed, + setExpandedDiffLines, + setSuggestPopoverDismissed, +} from '~/diffs/store/actions'; +import eventHub from '~/notes/event_hub'; +import * as types from '~/diffs/store/mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import testAction from '../../helpers/vuex_action_helper'; +import * as utils from '~/diffs/store/utils'; +import * as commonUtils from '~/lib/utils/common_utils'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import createFlash from '~/flash'; + +jest.mock('~/flash', () => jest.fn()); + +describe('DiffsStoreActions', () => { + useLocalStorageSpy(); + + const originalMethods = { + requestAnimationFrame: global.requestAnimationFrame, + requestIdleCallback: global.requestIdleCallback, + }; + + beforeEach(() => { + jest.spyOn(window.history, 'pushState'); + jest.spyOn(commonUtils, 'historyPushState'); + jest.spyOn(commonUtils, 'handleLocationHash').mockImplementation(() => null); + jest.spyOn(commonUtils, 'scrollToElement').mockImplementation(() => null); + jest.spyOn(utils, 'convertExpandLines').mockImplementation(() => null); + jest.spyOn(utils, 'idleCallback').mockImplementation(() => null); + ['requestAnimationFrame', 'requestIdleCallback'].forEach(method => { + global[method] = cb => { + cb(); + }; + }); + }); + + afterEach(() => { + ['requestAnimationFrame', 'requestIdleCallback'].forEach(method => { + global[method] = originalMethods[method]; + }); + createFlash.mockClear(); + }); + + describe('setBaseConfig', () => { + it('should set given endpoint and project path', done => { + const endpoint = '/diffs/set/endpoint'; + const endpointMetadata = '/diffs/set/endpoint/metadata'; + const endpointBatch = '/diffs/set/endpoint/batch'; + const endpointCoverage = '/diffs/set/coverage_reports'; + const projectPath = '/root/project'; + const dismissEndpoint = '/-/user_callouts'; + const showSuggestPopover = false; + const useSingleDiffStyle = false; + + testAction( + setBaseConfig, + { + endpoint, + endpointBatch, + endpointMetadata, + endpointCoverage, + projectPath, + dismissEndpoint, + showSuggestPopover, + useSingleDiffStyle, + }, + { + endpoint: '', + endpointBatch: '', + endpointMetadata: '', + endpointCoverage: '', + projectPath: '', + dismissEndpoint: '', + showSuggestPopover: true, + useSingleDiffStyle: true, + }, + [ + { + type: types.SET_BASE_CONFIG, + payload: { + endpoint, + endpointMetadata, + endpointBatch, + endpointCoverage, + projectPath, + dismissEndpoint, + showSuggestPopover, + useSingleDiffStyle, + }, + }, + ], + [], + done, + ); + }); + }); + + describe('fetchDiffFiles', () => { + it('should fetch diff files', done => { + const endpoint = '/fetch/diff/files?view=inline&w=1'; + const mock = new MockAdapter(axios); + const res = { diff_files: 1, merge_request_diffs: [] }; + mock.onGet(endpoint).reply(200, res); + + testAction( + fetchDiffFiles, + {}, + { endpoint, diffFiles: [], showWhitespace: false, diffViewType: 'inline' }, + [ + { 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(); + }, + ); + + fetchDiffFiles({ state: { endpoint }, commit: () => null }) + .then(data => { + expect(data).toEqual(res); + done(); + }) + .catch(done.fail); + }); + }); + + describe('fetchDiffFilesBatch', () => { + it('should fetch batch diff files', done => { + const endpointBatch = '/fetch/diffs_batch'; + const mock = new MockAdapter(axios); + const res1 = { diff_files: [], pagination: { next_page: 2 } }; + const res2 = { diff_files: [], pagination: {} }; + mock + .onGet(endpointBatch, { + params: { page: 1, per_page: DIFFS_PER_PAGE, w: '1', view: 'inline' }, + }) + .reply(200, res1) + .onGet(endpointBatch, { + params: { page: 2, per_page: DIFFS_PER_PAGE, w: '1', view: 'inline' }, + }) + .reply(200, res2); + + testAction( + fetchDiffFilesBatch, + {}, + { endpointBatch, useSingleDiffStyle: true, diffViewType: 'inline' }, + [ + { type: types.SET_BATCH_LOADING, payload: true }, + { type: types.SET_RETRIEVING_BATCHES, payload: true }, + { type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } }, + { type: types.SET_BATCH_LOADING, payload: false }, + { type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: [] } }, + { type: types.SET_BATCH_LOADING, payload: false }, + { type: types.SET_RETRIEVING_BATCHES, payload: false }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('fetchDiffFilesMeta', () => { + it('should fetch diff meta information', done => { + const endpointMetadata = '/fetch/diffs_meta?view=inline'; + const mock = new MockAdapter(axios); + const data = { diff_files: [] }; + const res = { data }; + mock.onGet(endpointMetadata).reply(200, res); + + testAction( + fetchDiffFilesMeta, + {}, + { endpointMetadata }, + [ + { type: types.SET_LOADING, payload: true }, + { type: types.SET_LOADING, payload: false }, + { type: types.SET_MERGE_REQUEST_DIFFS, payload: [] }, + { type: types.SET_DIFF_DATA, payload: { data } }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('when the single diff view feature flag is off', () => { + describe('fetchDiffFiles', () => { + it('should fetch diff files', done => { + const endpoint = '/fetch/diff/files?w=1'; + const mock = new MockAdapter(axios); + const res = { diff_files: 1, merge_request_diffs: [] }; + mock.onGet(endpoint).reply(200, res); + + testAction( + fetchDiffFiles, + {}, + { + endpoint, + diffFiles: [], + showWhitespace: false, + diffViewType: 'inline', + useSingleDiffStyle: false, + }, + [ + { 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(); + }, + ); + + fetchDiffFiles({ state: { endpoint }, commit: () => null }) + .then(data => { + expect(data).toEqual(res); + done(); + }) + .catch(done.fail); + }); + }); + + describe('fetchDiffFilesBatch', () => { + it('should fetch batch diff files', done => { + const endpointBatch = '/fetch/diffs_batch'; + const mock = new MockAdapter(axios); + const res1 = { diff_files: [], pagination: { next_page: 2 } }; + const res2 = { diff_files: [], pagination: {} }; + mock + .onGet(endpointBatch, { params: { page: 1, per_page: DIFFS_PER_PAGE, w: '1' } }) + .reply(200, res1) + .onGet(endpointBatch, { params: { page: 2, per_page: DIFFS_PER_PAGE, w: '1' } }) + .reply(200, res2); + + testAction( + fetchDiffFilesBatch, + {}, + { endpointBatch, useSingleDiffStyle: false }, + [ + { type: types.SET_BATCH_LOADING, payload: true }, + { type: types.SET_RETRIEVING_BATCHES, payload: true }, + { type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } }, + { type: types.SET_BATCH_LOADING, payload: false }, + { type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: [] } }, + { type: types.SET_BATCH_LOADING, payload: false }, + { type: types.SET_RETRIEVING_BATCHES, payload: false }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('fetchDiffFilesMeta', () => { + it('should fetch diff meta information', done => { + const endpointMetadata = '/fetch/diffs_meta?'; + const mock = new MockAdapter(axios); + const data = { diff_files: [] }; + const res = { data }; + mock.onGet(endpointMetadata).reply(200, res); + + testAction( + fetchDiffFilesMeta, + {}, + { endpointMetadata, useSingleDiffStyle: false }, + [ + { type: types.SET_LOADING, payload: true }, + { type: types.SET_LOADING, payload: false }, + { type: types.SET_MERGE_REQUEST_DIFFS, payload: [] }, + { type: types.SET_DIFF_DATA, payload: { data } }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + }); + + describe('fetchCoverageFiles', () => { + let mock; + const endpointCoverage = '/fetch'; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => mock.restore()); + + it('should commit SET_COVERAGE_DATA with received response', done => { + const data = { files: { 'app.js': { '1': 0, '2': 1 } } }; + + mock.onGet(endpointCoverage).reply(200, { data }); + + testAction( + fetchCoverageFiles, + {}, + { endpointCoverage }, + [{ type: types.SET_COVERAGE_DATA, payload: { data } }], + [], + done, + ); + }); + + it('should show flash on API error', done => { + mock.onGet(endpointCoverage).reply(400); + + testAction(fetchCoverageFiles, {}, { endpointCoverage }, [], [], () => { + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong')); + done(); + }); + }); + }); + + describe('setHighlightedRow', () => { + it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => { + testAction(setHighlightedRow, 'ABC_123', {}, [ + { type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' }, + { type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'ABC' }, + ]); + }); + }); + + describe('assignDiscussionsToDiff', () => { + it('should merge discussions into diffs', done => { + window.location.hash = 'ABC_123'; + + const state = { + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1_1', + discussions: [], + }, + right: { + line_code: 'ABC_1_1', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1_1', + discussions: [], + old_line: 5, + new_line: null, + }, + ], + diff_refs: { + base_sha: 'abc', + head_sha: 'def', + start_sha: 'ghi', + }, + new_path: 'file1', + old_path: 'file2', + }, + ], + }; + + const diffPosition = { + base_sha: 'abc', + head_sha: 'def', + start_sha: 'ghi', + new_line: null, + new_path: 'file1', + old_line: 5, + old_path: 'file2', + }; + + const singleDiscussion = { + line_code: 'ABC_1_1', + diff_discussion: {}, + diff_file: { + file_hash: 'ABC', + }, + file_hash: 'ABC', + resolvable: true, + position: diffPosition, + original_position: diffPosition, + }; + + const discussions = [singleDiscussion]; + + testAction( + assignDiscussionsToDiff, + discussions, + state, + [ + { + type: types.SET_LINE_DISCUSSIONS_FOR_FILE, + payload: { + discussion: singleDiscussion, + diffPositionByLineCode: { + ABC_1_1: { + base_sha: 'abc', + head_sha: 'def', + start_sha: 'ghi', + new_line: null, + new_path: 'file1', + old_line: 5, + old_path: 'file2', + line_code: 'ABC_1_1', + position_type: 'text', + }, + }, + hash: 'ABC_123', + }, + }, + ], + [], + done, + ); + }); + }); + + describe('removeDiscussionsFromDiff', () => { + it('should remove discussions from diffs', done => { + const state = { + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1_1', + discussions: [ + { + id: 1, + }, + ], + }, + right: { + line_code: 'ABC_1_1', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1_1', + discussions: [], + }, + ], + }, + ], + }; + const singleDiscussion = { + id: '1', + file_hash: 'ABC', + line_code: 'ABC_1_1', + }; + + testAction( + removeDiscussionsFromDiff, + singleDiscussion, + state, + [ + { + type: types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, + payload: { + id: '1', + fileHash: 'ABC', + lineCode: 'ABC_1_1', + }, + }, + ], + [], + done, + ); + }); + }); + + describe('startRenderDiffsQueue', () => { + it('should set all files to RENDER_FILE', () => { + const state = { + diffFiles: [ + { + id: 1, + renderIt: false, + viewer: { + collapsed: false, + }, + }, + { + id: 2, + renderIt: false, + viewer: { + collapsed: false, + }, + }, + ], + }; + + const pseudoCommit = (commitType, file) => { + expect(commitType).toBe(types.RENDER_FILE); + Object.assign(file, { + renderIt: true, + }); + }; + + startRenderDiffsQueue({ state, commit: pseudoCommit }); + + expect(state.diffFiles[0].renderIt).toBe(true); + expect(state.diffFiles[1].renderIt).toBe(true); + }); + }); + + describe('setInlineDiffViewType', () => { + it('should set diff view type to inline and also set the cookie properly', done => { + testAction( + setInlineDiffViewType, + null, + {}, + [{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }], + [], + () => { + setImmediate(() => { + expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE); + done(); + }); + }, + ); + }); + }); + + describe('setParallelDiffViewType', () => { + it('should set diff view type to parallel and also set the cookie properly', done => { + testAction( + setParallelDiffViewType, + null, + {}, + [{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }], + [], + () => { + setImmediate(() => { + expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE); + done(); + }); + }, + ); + }); + }); + + describe('showCommentForm', () => { + it('should call mutation to show comment form', done => { + const payload = { lineCode: 'lineCode', fileHash: 'hash' }; + + testAction( + showCommentForm, + payload, + {}, + [{ type: types.TOGGLE_LINE_HAS_FORM, payload: { ...payload, hasForm: true } }], + [], + done, + ); + }); + }); + + describe('cancelCommentForm', () => { + it('should call mutation to cancel comment form', done => { + const payload = { lineCode: 'lineCode', fileHash: 'hash' }; + + testAction( + cancelCommentForm, + payload, + {}, + [{ type: types.TOGGLE_LINE_HAS_FORM, payload: { ...payload, hasForm: false } }], + [], + 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 isExpandDown = false; + const nextLineNumbers = {}; + const options = { endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers }; + const mock = new MockAdapter(axios); + const contextLines = { contextLines: [{ lineCode: 6 }] }; + mock.onGet(endpoint).reply(200, contextLines); + + testAction( + loadMoreLines, + options, + {}, + [ + { + type: types.ADD_CONTEXT_LINES, + payload: { lineNumbers, contextLines, params, fileHash, isExpandDown, nextLineNumbers }, + }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('loadCollapsedDiff', () => { + const state = { showWhitespace: true }; + it('should fetch data and call mutation with response and the give parameter', done => { + const file = { hash: 123, load_collapsed_diff_url: '/load/collapsed/diff/url' }; + const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] }; + const mock = new MockAdapter(axios); + const commit = jest.fn(); + mock.onGet(file.loadCollapsedDiffUrl).reply(200, data); + + loadCollapsedDiff({ commit, getters: { commitId: null }, state }, file) + .then(() => { + expect(commit).toHaveBeenCalledWith(types.ADD_COLLAPSED_DIFFS, { file, data }); + + mock.restore(); + done(); + }) + .catch(done.fail); + }); + + it('should fetch data without commit ID', () => { + const file = { load_collapsed_diff_url: '/load/collapsed/diff/url' }; + const getters = { + commitId: null, + }; + + jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} })); + + loadCollapsedDiff({ commit() {}, getters, state }, file); + + expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, { + params: { commit_id: null, w: '0' }, + }); + }); + + it('should fetch data with commit ID', () => { + const file = { load_collapsed_diff_url: '/load/collapsed/diff/url' }; + const getters = { + commitId: '123', + }; + + jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} })); + + loadCollapsedDiff({ commit() {}, getters, state }, file); + + expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, { + params: { commit_id: '123', w: '0' }, + }); + }); + }); + + describe('expandAllFiles', () => { + it('should change the collapsed prop from the diffFiles', done => { + testAction( + expandAllFiles, + null, + {}, + [ + { + type: types.EXPAND_ALL_FILES, + }, + ], + [], + done, + ); + }); + }); + + describe('toggleFileDiscussions', () => { + it('should dispatch collapseDiscussion when all discussions are expanded', () => { + const getters = { + getDiffFileDiscussions: jest.fn(() => [{ id: 1 }]), + diffHasAllExpandedDiscussions: jest.fn(() => true), + diffHasAllCollapsedDiscussions: jest.fn(() => false), + }; + + const dispatch = jest.fn(); + + toggleFileDiscussions({ getters, dispatch }); + + expect(dispatch).toHaveBeenCalledWith( + 'collapseDiscussion', + { discussionId: 1 }, + { root: true }, + ); + }); + + it('should dispatch expandDiscussion when all discussions are collapsed', () => { + const getters = { + getDiffFileDiscussions: jest.fn(() => [{ id: 1 }]), + diffHasAllExpandedDiscussions: jest.fn(() => false), + diffHasAllCollapsedDiscussions: jest.fn(() => true), + }; + + const dispatch = jest.fn(); + + toggleFileDiscussions({ getters, dispatch }); + + expect(dispatch).toHaveBeenCalledWith( + 'expandDiscussion', + { discussionId: 1 }, + { root: true }, + ); + }); + + it('should dispatch expandDiscussion when some discussions are collapsed and others are expanded for the collapsed discussion', () => { + const getters = { + getDiffFileDiscussions: jest.fn(() => [{ expanded: false, id: 1 }]), + diffHasAllExpandedDiscussions: jest.fn(() => false), + diffHasAllCollapsedDiscussions: jest.fn(() => false), + }; + + const dispatch = jest.fn(); + + toggleFileDiscussions({ getters, dispatch }); + + expect(dispatch).toHaveBeenCalledWith( + 'expandDiscussion', + { discussionId: 1 }, + { root: true }, + ); + }); + }); + + describe('scrollToLineIfNeededInline', () => { + const lineMock = { + line_code: 'ABC_123', + }; + + it('should not call handleLocationHash when there is not hash', () => { + window.location.hash = ''; + + scrollToLineIfNeededInline({}, lineMock); + + expect(commonUtils.handleLocationHash).not.toHaveBeenCalled(); + }); + + it('should not call handleLocationHash when the hash does not match any line', () => { + window.location.hash = 'XYZ_456'; + + scrollToLineIfNeededInline({}, lineMock); + + expect(commonUtils.handleLocationHash).not.toHaveBeenCalled(); + }); + + it('should call handleLocationHash only when the hash matches a line', () => { + window.location.hash = 'ABC_123'; + + scrollToLineIfNeededInline( + {}, + { + lineCode: 'ABC_456', + }, + ); + scrollToLineIfNeededInline({}, lineMock); + scrollToLineIfNeededInline( + {}, + { + lineCode: 'XYZ_456', + }, + ); + + expect(commonUtils.handleLocationHash).toHaveBeenCalled(); + expect(commonUtils.handleLocationHash).toHaveBeenCalledTimes(1); + }); + }); + + describe('scrollToLineIfNeededParallel', () => { + const lineMock = { + left: null, + right: { + line_code: 'ABC_123', + }, + }; + + it('should not call handleLocationHash when there is not hash', () => { + window.location.hash = ''; + + scrollToLineIfNeededParallel({}, lineMock); + + expect(commonUtils.handleLocationHash).not.toHaveBeenCalled(); + }); + + it('should not call handleLocationHash when the hash does not match any line', () => { + window.location.hash = 'XYZ_456'; + + scrollToLineIfNeededParallel({}, lineMock); + + expect(commonUtils.handleLocationHash).not.toHaveBeenCalled(); + }); + + it('should call handleLocationHash only when the hash matches a line', () => { + window.location.hash = 'ABC_123'; + + scrollToLineIfNeededParallel( + {}, + { + left: null, + right: { + lineCode: 'ABC_456', + }, + }, + ); + scrollToLineIfNeededParallel({}, lineMock); + scrollToLineIfNeededParallel( + {}, + { + left: null, + right: { + lineCode: 'XYZ_456', + }, + }, + ); + + expect(commonUtils.handleLocationHash).toHaveBeenCalled(); + expect(commonUtils.handleLocationHash).toHaveBeenCalledTimes(1); + }); + }); + + describe('saveDiffDiscussion', () => { + it('dispatches actions', done => { + const commitId = 'something'; + const formData = { + diffFile: { ...mockDiffFile }, + noteableData: {}, + }; + const note = {}; + const state = { + commit: { + id: commitId, + }, + }; + const dispatch = jest.fn(name => { + switch (name) { + case 'saveNote': + return Promise.resolve({ + discussion: 'test', + }); + case 'updateDiscussion': + return Promise.resolve('discussion'); + default: + return Promise.resolve({}); + } + }); + + saveDiffDiscussion({ state, dispatch }, { note, formData }) + .then(() => { + expect(dispatch).toHaveBeenCalledTimes(5); + expect(dispatch).toHaveBeenNthCalledWith(1, 'saveNote', expect.any(Object), { + root: true, + }); + + const postData = dispatch.mock.calls[0][1]; + expect(postData.data.note.commit_id).toBe(commitId); + + expect(dispatch).toHaveBeenNthCalledWith(2, 'updateDiscussion', 'test', { root: true }); + expect(dispatch).toHaveBeenNthCalledWith(3, 'assignDiscussionsToDiff', ['discussion']); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('toggleTreeOpen', () => { + it('commits TOGGLE_FOLDER_OPEN', done => { + testAction( + toggleTreeOpen, + 'path', + {}, + [{ type: types.TOGGLE_FOLDER_OPEN, payload: 'path' }], + [], + done, + ); + }); + }); + + describe('scrollToFile', () => { + let commit; + + beforeEach(() => { + commit = jest.fn(); + }); + + it('updates location hash', () => { + const state = { + treeEntries: { + path: { + fileHash: 'test', + }, + }, + }; + + scrollToFile({ state, commit }, 'path'); + + expect(document.location.hash).toBe('#test'); + }); + + it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => { + const state = { + treeEntries: { + path: { + fileHash: 'test', + }, + }, + }; + + scrollToFile({ state, commit }, 'path'); + + expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, 'test'); + }); + }); + + describe('toggleShowTreeList', () => { + it('commits toggle', done => { + testAction(toggleShowTreeList, null, {}, [{ type: types.TOGGLE_SHOW_TREE_LIST }], [], done); + }); + + it('updates localStorage', () => { + jest.spyOn(localStorage, 'setItem').mockImplementation(() => {}); + + toggleShowTreeList({ commit() {}, state: { showTreeList: true } }); + + expect(localStorage.setItem).toHaveBeenCalledWith('mr_tree_show', true); + }); + + it('does not update localStorage', () => { + jest.spyOn(localStorage, 'setItem').mockImplementation(() => {}); + + toggleShowTreeList({ commit() {}, state: { showTreeList: true } }, false); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); + }); + + describe('renderFileForDiscussionId', () => { + const rootState = { + notes: { + discussions: [ + { + id: '123', + diff_file: { + file_hash: 'HASH', + }, + }, + { + id: '456', + diff_file: { + file_hash: 'HASH', + }, + }, + ], + }, + }; + let commit; + let $emit; + const state = ({ collapsed, renderIt }) => ({ + diffFiles: [ + { + file_hash: 'HASH', + viewer: { + collapsed, + }, + renderIt, + }, + ], + }); + + beforeEach(() => { + commit = jest.fn(); + $emit = jest.spyOn(eventHub, '$emit'); + }); + + it('renders and expands file for the given discussion id', () => { + const localState = state({ collapsed: true, renderIt: false }); + + renderFileForDiscussionId({ rootState, state: localState, commit }, '123'); + + expect(commit).toHaveBeenCalledWith('RENDER_FILE', localState.diffFiles[0]); + expect($emit).toHaveBeenCalledTimes(1); + expect(commonUtils.scrollToElement).toHaveBeenCalledTimes(1); + }); + + it('jumps to discussion on already rendered and expanded file', () => { + const localState = state({ collapsed: false, renderIt: true }); + + renderFileForDiscussionId({ rootState, state: localState, commit }, '123'); + + expect(commit).not.toHaveBeenCalled(); + expect($emit).toHaveBeenCalledTimes(1); + expect(commonUtils.scrollToElement).not.toHaveBeenCalled(); + }); + }); + + describe('setRenderTreeList', () => { + it('commits SET_RENDER_TREE_LIST', done => { + testAction( + setRenderTreeList, + true, + {}, + [{ type: types.SET_RENDER_TREE_LIST, payload: true }], + [], + done, + ); + }); + + it('sets localStorage', () => { + setRenderTreeList({ commit() {} }, true); + + expect(localStorage.setItem).toHaveBeenCalledWith('mr_diff_tree_list', true); + }); + }); + + describe('setShowWhitespace', () => { + beforeEach(() => { + jest.spyOn(eventHub, '$emit').mockImplementation(); + }); + + it('commits SET_SHOW_WHITESPACE', done => { + testAction( + setShowWhitespace, + { showWhitespace: true }, + {}, + [{ type: types.SET_SHOW_WHITESPACE, payload: true }], + [], + done, + ); + }); + + it('sets localStorage', () => { + setShowWhitespace({ commit() {} }, { showWhitespace: true }); + + expect(localStorage.setItem).toHaveBeenCalledWith('mr_show_whitespace', true); + }); + + it('calls history pushState', () => { + setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true }); + + expect(window.history.pushState).toHaveBeenCalled(); + }); + + it('calls history pushState with merged params', () => { + window.history.pushState({}, '', '?test=1'); + + setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true }); + + expect( + window.history.pushState.mock.calls[window.history.pushState.mock.calls.length - 1][2], + ).toMatch(/(.*)\?test=1&w=0/); + + window.history.pushState({}, '', '?'); + }); + + it('emits eventHub event', () => { + setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true }); + + expect(eventHub.$emit).toHaveBeenCalledWith('refetchDiffData'); + }); + }); + + describe('setRenderIt', () => { + it('commits RENDER_FILE', done => { + testAction(setRenderIt, 'file', {}, [{ type: types.RENDER_FILE, payload: 'file' }], [], done); + }); + }); + + describe('requestFullDiff', () => { + it('commits REQUEST_FULL_DIFF', done => { + testAction( + requestFullDiff, + 'file', + {}, + [{ type: types.REQUEST_FULL_DIFF, payload: 'file' }], + [], + done, + ); + }); + }); + + describe('receiveFullDiffSucess', () => { + it('commits REQUEST_FULL_DIFF', done => { + testAction( + receiveFullDiffSucess, + { filePath: 'test' }, + {}, + [{ type: types.RECEIVE_FULL_DIFF_SUCCESS, payload: { filePath: 'test' } }], + [], + done, + ); + }); + }); + + describe('receiveFullDiffError', () => { + it('commits REQUEST_FULL_DIFF', done => { + testAction( + receiveFullDiffError, + 'file', + {}, + [{ type: types.RECEIVE_FULL_DIFF_ERROR, payload: 'file' }], + [], + done, + ); + }); + }); + + describe('fetchFullDiff', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + beforeEach(() => { + mock.onGet(`${gl.TEST_HOST}/context`).replyOnce(200, ['test']); + }); + + it('dispatches receiveFullDiffSucess', done => { + const file = { + context_lines_path: `${gl.TEST_HOST}/context`, + file_path: 'test', + file_hash: 'test', + }; + testAction( + fetchFullDiff, + file, + null, + [], + [ + { type: 'receiveFullDiffSucess', payload: { filePath: 'test' } }, + { type: 'setExpandedDiffLines', payload: { file, data: ['test'] } }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${gl.TEST_HOST}/context`).replyOnce(500); + }); + + it('dispatches receiveFullDiffError', done => { + testAction( + fetchFullDiff, + { context_lines_path: `${gl.TEST_HOST}/context`, file_path: 'test', file_hash: 'test' }, + null, + [], + [{ type: 'receiveFullDiffError', payload: 'test' }], + done, + ); + }); + }); + }); + + describe('toggleFullDiff', () => { + let state; + + beforeEach(() => { + state = { + diffFiles: [{ file_path: 'test', isShowingFullFile: false }], + }; + }); + + it('dispatches fetchFullDiff when file is not expanded', done => { + testAction( + toggleFullDiff, + 'test', + state, + [], + [ + { type: 'requestFullDiff', payload: 'test' }, + { type: 'fetchFullDiff', payload: state.diffFiles[0] }, + ], + done, + ); + }); + }); + + describe('setFileCollapsed', () => { + it('commits SET_FILE_COLLAPSED', done => { + testAction( + setFileCollapsed, + { filePath: 'test', collapsed: true }, + null, + [{ type: types.SET_FILE_COLLAPSED, payload: { filePath: 'test', collapsed: true } }], + [], + done, + ); + }); + }); + + describe('setExpandedDiffLines', () => { + beforeEach(() => { + utils.idleCallback.mockImplementation(cb => { + cb({ timeRemaining: () => 50 }); + }); + }); + + it('commits SET_CURRENT_VIEW_DIFF_FILE_LINES when lines less than MAX_RENDERING_DIFF_LINES', done => { + utils.convertExpandLines.mockImplementation(() => ['test']); + + testAction( + setExpandedDiffLines, + { file: { file_path: 'path' }, data: [] }, + { diffViewType: 'inline' }, + [ + { + type: 'SET_HIDDEN_VIEW_DIFF_FILE_LINES', + payload: { filePath: 'path', lines: ['test'] }, + }, + { + type: 'SET_CURRENT_VIEW_DIFF_FILE_LINES', + payload: { filePath: 'path', lines: ['test'] }, + }, + ], + [], + done, + ); + }); + + it('commits ADD_CURRENT_VIEW_DIFF_FILE_LINES when lines more than MAX_RENDERING_DIFF_LINES', done => { + const lines = new Array(501).fill().map((_, i) => `line-${i}`); + utils.convertExpandLines.mockReturnValue(lines); + + testAction( + setExpandedDiffLines, + { file: { file_path: 'path' }, data: [] }, + { diffViewType: 'inline' }, + [ + { + type: 'SET_HIDDEN_VIEW_DIFF_FILE_LINES', + payload: { filePath: 'path', lines }, + }, + { + type: 'SET_CURRENT_VIEW_DIFF_FILE_LINES', + payload: { filePath: 'path', lines: lines.slice(0, 200) }, + }, + { type: 'TOGGLE_DIFF_FILE_RENDERING_MORE', payload: 'path' }, + ...new Array(301).fill().map((_, i) => ({ + type: 'ADD_CURRENT_VIEW_DIFF_FILE_LINES', + payload: { filePath: 'path', line: `line-${i + 200}` }, + })), + { type: 'TOGGLE_DIFF_FILE_RENDERING_MORE', payload: 'path' }, + ], + [], + done, + ); + }); + }); + + describe('setSuggestPopoverDismissed', () => { + it('commits SET_SHOW_SUGGEST_POPOVER', done => { + const state = { dismissEndpoint: `${gl.TEST_HOST}/-/user_callouts` }; + const mock = new MockAdapter(axios); + mock.onPost(state.dismissEndpoint).reply(200, {}); + + jest.spyOn(axios, 'post'); + + testAction( + setSuggestPopoverDismissed, + null, + state, + [{ type: types.SET_SHOW_SUGGEST_POPOVER }], + [], + () => { + expect(axios.post).toHaveBeenCalledWith(state.dismissEndpoint, { + feature_name: 'suggest_popover_dismissed', + }); + + mock.restore(); + done(); + }, + ); + }); + }); +}); diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js new file mode 100644 index 00000000000..ca47f51cb15 --- /dev/null +++ b/spec/frontend/diffs/store/getters_spec.js @@ -0,0 +1,315 @@ +import * as getters from '~/diffs/store/getters'; +import state from '~/diffs/store/modules/diff_state'; +import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; +import discussion from '../mock_data/diff_discussions'; + +describe('Diffs Module Getters', () => { + let localState; + let discussionMock; + let discussionMock1; + + const diffFileMock = { + fileHash: '9732849daca6ae818696d9575f5d1207d1a7f8bb', + }; + + beforeEach(() => { + localState = state(); + discussionMock = Object.assign({}, discussion); + discussionMock.diff_file.file_hash = diffFileMock.fileHash; + + discussionMock1 = Object.assign({}, discussion); + discussionMock1.diff_file.file_hash = diffFileMock.fileHash; + }); + + describe('isParallelView', () => { + it('should return true if view set to parallel view', () => { + localState.diffViewType = PARALLEL_DIFF_VIEW_TYPE; + + expect(getters.isParallelView(localState)).toEqual(true); + }); + + it('should return false if view not to parallel view', () => { + localState.diffViewType = INLINE_DIFF_VIEW_TYPE; + + expect(getters.isParallelView(localState)).toEqual(false); + }); + }); + + describe('isInlineView', () => { + it('should return true if view set to inline view', () => { + localState.diffViewType = INLINE_DIFF_VIEW_TYPE; + + expect(getters.isInlineView(localState)).toEqual(true); + }); + + it('should return false if view not to inline view', () => { + localState.diffViewType = PARALLEL_DIFF_VIEW_TYPE; + + expect(getters.isInlineView(localState)).toEqual(false); + }); + }); + + describe('hasCollapsedFile', () => { + it('returns true when all files are collapsed', () => { + localState.diffFiles = [{ viewer: { collapsed: true } }, { viewer: { collapsed: true } }]; + + expect(getters.hasCollapsedFile(localState)).toEqual(true); + }); + + it('returns true when at least one file is collapsed', () => { + localState.diffFiles = [{ viewer: { collapsed: false } }, { viewer: { collapsed: true } }]; + + expect(getters.hasCollapsedFile(localState)).toEqual(true); + }); + }); + + describe('commitId', () => { + it('returns commit id when is set', () => { + const commitID = '800f7a91'; + localState.commit = { + id: commitID, + }; + + expect(getters.commitId(localState)).toEqual(commitID); + }); + + it('returns null when no commit is set', () => { + expect(getters.commitId(localState)).toEqual(null); + }); + }); + + describe('diffHasAllExpandedDiscussions', () => { + it('returns true when all discussions are expanded', () => { + expect( + getters.diffHasAllExpandedDiscussions(localState, { + getDiffFileDiscussions: () => [discussionMock, discussionMock], + })(diffFileMock), + ).toEqual(true); + }); + + it('returns false when there are no discussions', () => { + expect( + getters.diffHasAllExpandedDiscussions(localState, { + getDiffFileDiscussions: () => [], + })(diffFileMock), + ).toEqual(false); + }); + + it('returns false when one discussions is collapsed', () => { + discussionMock1.expanded = false; + + expect( + getters.diffHasAllExpandedDiscussions(localState, { + getDiffFileDiscussions: () => [discussionMock, discussionMock1], + })(diffFileMock), + ).toEqual(false); + }); + }); + + describe('diffHasAllCollapsedDiscussions', () => { + it('returns true when all discussions are collapsed', () => { + discussionMock.diff_file.file_hash = diffFileMock.fileHash; + discussionMock.expanded = false; + + expect( + getters.diffHasAllCollapsedDiscussions(localState, { + getDiffFileDiscussions: () => [discussionMock], + })(diffFileMock), + ).toEqual(true); + }); + + it('returns false when there are no discussions', () => { + expect( + getters.diffHasAllCollapsedDiscussions(localState, { + getDiffFileDiscussions: () => [], + })(diffFileMock), + ).toEqual(false); + }); + + it('returns false when one discussions is expanded', () => { + discussionMock1.expanded = false; + + expect( + getters.diffHasAllCollapsedDiscussions(localState, { + getDiffFileDiscussions: () => [discussionMock, discussionMock1], + })(diffFileMock), + ).toEqual(false); + }); + }); + + describe('diffHasExpandedDiscussions', () => { + it('returns true when one of the discussions is expanded', () => { + discussionMock1.expanded = false; + + expect( + getters.diffHasExpandedDiscussions(localState, { + getDiffFileDiscussions: () => [discussionMock, discussionMock], + })(diffFileMock), + ).toEqual(true); + }); + + it('returns false when there are no discussions', () => { + expect( + getters.diffHasExpandedDiscussions(localState, { getDiffFileDiscussions: () => [] })( + diffFileMock, + ), + ).toEqual(false); + }); + + it('returns false when no discussion is expanded', () => { + discussionMock.expanded = false; + discussionMock1.expanded = false; + + expect( + getters.diffHasExpandedDiscussions(localState, { + getDiffFileDiscussions: () => [discussionMock, discussionMock1], + })(diffFileMock), + ).toEqual(false); + }); + }); + + describe('diffHasDiscussions', () => { + it('returns true when getDiffFileDiscussions returns discussions', () => { + expect( + getters.diffHasDiscussions(localState, { + getDiffFileDiscussions: () => [discussionMock], + })(diffFileMock), + ).toEqual(true); + }); + + it('returns false when getDiffFileDiscussions returns no discussions', () => { + expect( + getters.diffHasDiscussions(localState, { + getDiffFileDiscussions: () => [], + })(diffFileMock), + ).toEqual(false); + }); + }); + + describe('getDiffFileDiscussions', () => { + it('returns an array with discussions when fileHash matches and the discussion belongs to a diff', () => { + discussionMock.diff_file.file_hash = diffFileMock.file_hash; + + expect( + getters.getDiffFileDiscussions(localState, {}, {}, { discussions: [discussionMock] })( + diffFileMock, + ).length, + ).toEqual(1); + }); + + it('returns an empty array when no discussions are found in the given diff', () => { + expect( + getters.getDiffFileDiscussions(localState, {}, {}, { discussions: [] })(diffFileMock) + .length, + ).toEqual(0); + }); + }); + + describe('getDiffFileByHash', () => { + it('returns file by hash', () => { + const fileA = { + file_hash: '123', + }; + const fileB = { + file_hash: '456', + }; + localState.diffFiles = [fileA, fileB]; + + expect(getters.getDiffFileByHash(localState)('456')).toEqual(fileB); + }); + + it('returns null if no matching file is found', () => { + localState.diffFiles = []; + + expect(getters.getDiffFileByHash(localState)('123')).toBeUndefined(); + }); + }); + + describe('allBlobs', () => { + it('returns an array of blobs', () => { + localState.treeEntries = { + file: { + type: 'blob', + path: 'file', + parentPath: '/', + tree: [], + }, + tree: { + type: 'tree', + path: 'tree', + parentPath: '/', + tree: [], + }, + }; + + expect( + getters.allBlobs(localState, { + flatBlobsList: getters.flatBlobsList(localState), + }), + ).toEqual([ + { + isHeader: true, + path: '/', + tree: [ + { + parentPath: '/', + path: 'file', + tree: [], + type: 'blob', + }, + ], + }, + ]); + }); + }); + + describe('currentDiffIndex', () => { + it('returns index of currently selected diff in diffList', () => { + localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }]; + localState.currentDiffFileId = '222'; + + expect(getters.currentDiffIndex(localState)).toEqual(1); + + localState.currentDiffFileId = '333'; + + expect(getters.currentDiffIndex(localState)).toEqual(2); + }); + + it('returns 0 if no diff is selected yet or diff is not found', () => { + localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }]; + localState.currentDiffFileId = ''; + + expect(getters.currentDiffIndex(localState)).toEqual(0); + }); + }); + + describe('fileLineCoverage', () => { + beforeEach(() => { + Object.assign(localState.coverageFiles, { files: { 'app.js': { '1': 0, '2': 5 } } }); + }); + + it('returns empty object when no coverage data is available', () => { + Object.assign(localState.coverageFiles, {}); + + expect(getters.fileLineCoverage(localState)('test.js', 2)).toEqual({}); + }); + + it('returns empty object when unknown filename is passed', () => { + expect(getters.fileLineCoverage(localState)('test.js', 2)).toEqual({}); + }); + + it('returns no-coverage info when correct filename and line is passed', () => { + expect(getters.fileLineCoverage(localState)('app.js', 1)).toEqual({ + text: 'No test coverage', + class: 'no-coverage', + }); + }); + + it('returns coverage info when correct filename and line is passed', () => { + expect(getters.fileLineCoverage(localState)('app.js', 2)).toEqual({ + text: 'Test coverage: 5 hits', + class: 'coverage', + }); + }); + }); +}); diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js new file mode 100644 index 00000000000..f486a53fc4d --- /dev/null +++ b/spec/frontend/diffs/store/mutations_spec.js @@ -0,0 +1,966 @@ +import createState from '~/diffs/store/modules/diff_state'; +import mutations from '~/diffs/store/mutations'; +import * as types from '~/diffs/store/mutation_types'; +import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; +import diffFileMockData from '../mock_data/diff_file'; +import * as utils from '~/diffs/store/utils'; + +describe('DiffsStoreMutations', () => { + describe('SET_BASE_CONFIG', () => { + it('should set endpoint and project path', () => { + const state = {}; + const endpoint = '/diffs/endpoint'; + const projectPath = '/root/project'; + const useSingleDiffStyle = false; + + mutations[types.SET_BASE_CONFIG](state, { endpoint, projectPath, useSingleDiffStyle }); + + expect(state.endpoint).toEqual(endpoint); + expect(state.projectPath).toEqual(projectPath); + expect(state.useSingleDiffStyle).toEqual(useSingleDiffStyle); + }); + }); + + describe('SET_LOADING', () => { + it('should set loading state', () => { + const state = {}; + + mutations[types.SET_LOADING](state, false); + + expect(state.isLoading).toEqual(false); + }); + }); + + describe('SET_BATCH_LOADING', () => { + it('should set loading state', () => { + const state = {}; + + mutations[types.SET_BATCH_LOADING](state, false); + + expect(state.isBatchLoading).toEqual(false); + }); + }); + + describe('SET_RETRIEVING_BATCHES', () => { + it('should set retrievingBatches state', () => { + const state = {}; + + mutations[types.SET_RETRIEVING_BATCHES](state, false); + + expect(state.retrievingBatches).toEqual(false); + }); + }); + + describe('SET_DIFF_DATA', () => { + it('should set diff data type properly', () => { + const state = { + diffFiles: [ + { + ...diffFileMockData, + parallel_diff_lines: [], + }, + ], + }; + const diffMock = { + diff_files: [diffFileMockData], + }; + + mutations[types.SET_DIFF_DATA](state, diffMock); + + const firstLine = state.diffFiles[0].parallel_diff_lines[0]; + + expect(firstLine.right.text).toBeUndefined(); + expect(state.diffFiles.length).toEqual(1); + expect(state.diffFiles[0].renderIt).toEqual(true); + expect(state.diffFiles[0].collapsed).toEqual(false); + }); + + describe('given diffsBatchLoad feature flag is enabled', () => { + beforeEach(() => { + gon.features = { diffsBatchLoad: true }; + }); + + afterEach(() => { + delete gon.features; + }); + + it('should not modify the existing state', () => { + const state = { + diffFiles: [ + { + content_sha: diffFileMockData.content_sha, + file_hash: diffFileMockData.file_hash, + highlighted_diff_lines: [], + }, + ], + }; + const diffMock = { + diff_files: [diffFileMockData], + }; + + mutations[types.SET_DIFF_DATA](state, diffMock); + + // If the batch load is enabled, there shouldn't be any processing + // done on the existing state object, so we shouldn't have this. + expect(state.diffFiles[0].parallel_diff_lines).toBeUndefined(); + }); + }); + }); + + describe('SET_DIFFSET_DIFF_DATA_BATCH_DATA', () => { + it('should set diff data batch type properly', () => { + const state = { diffFiles: [] }; + const diffMock = { + diff_files: [diffFileMockData], + }; + + mutations[types.SET_DIFF_DATA_BATCH](state, diffMock); + + const firstLine = state.diffFiles[0].parallel_diff_lines[0]; + + expect(firstLine.right.text).toBeUndefined(); + expect(state.diffFiles[0].renderIt).toEqual(true); + expect(state.diffFiles[0].collapsed).toEqual(false); + }); + }); + + describe('SET_COVERAGE_DATA', () => { + it('should set coverage data properly', () => { + const state = { coverageFiles: {} }; + const coverage = { 'app.js': { '1': 0, '2': 1 } }; + + mutations[types.SET_COVERAGE_DATA](state, coverage); + + expect(state.coverageFiles).toEqual(coverage); + }); + }); + + 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('EXPAND_ALL_FILES', () => { + it('should change the collapsed prop from diffFiles', () => { + const diffFile = { + viewer: { + collapsed: true, + }, + }; + const state = { expandAllFiles: true, diffFiles: [diffFile] }; + + mutations[types.EXPAND_ALL_FILES](state); + + expect(state.diffFiles[0].viewer.collapsed).toEqual(false); + }); + }); + + describe('ADD_CONTEXT_LINES', () => { + it('should call utils.addContextLines with proper params', () => { + const options = { + lineNumbers: { oldLineNumber: 1, newLineNumber: 2 }, + contextLines: [ + { old_line: 1, new_line: 1, line_code: 'ff9200_1_1', discussions: [], hasForm: false }, + ], + fileHash: 'ff9200', + params: { + bottom: true, + }, + isExpandDown: false, + nextLineNumbers: {}, + }; + const diffFile = { + file_hash: options.fileHash, + highlighted_diff_lines: [], + parallel_diff_lines: [], + }; + const state = { diffFiles: [diffFile], diffViewType: 'viewType' }; + const lines = [{ old_line: 1, new_line: 1 }]; + + jest.spyOn(utils, 'findDiffFile').mockImplementation(() => diffFile); + jest.spyOn(utils, 'removeMatchLine').mockImplementation(() => null); + jest.spyOn(utils, 'addLineReferences').mockImplementation(() => lines); + jest.spyOn(utils, 'addContextLines').mockImplementation(() => null); + + mutations[types.ADD_CONTEXT_LINES](state, options); + + expect(utils.findDiffFile).toHaveBeenCalledWith(state.diffFiles, options.fileHash); + expect(utils.removeMatchLine).toHaveBeenCalledWith( + diffFile, + options.lineNumbers, + options.params.bottom, + ); + + expect(utils.addLineReferences).toHaveBeenCalledWith( + options.contextLines, + options.lineNumbers, + options.params.bottom, + options.isExpandDown, + options.nextLineNumbers, + ); + + expect(utils.addContextLines).toHaveBeenCalledWith({ + inlineLines: diffFile.highlighted_diff_lines, + parallelLines: diffFile.parallel_diff_lines, + diffViewType: 'viewType', + contextLines: options.contextLines, + bottom: options.params.bottom, + lineNumbers: options.lineNumbers, + isExpandDown: false, + }); + }); + }); + + describe('ADD_COLLAPSED_DIFFS', () => { + it('should update the state with the given data for the given file hash', () => { + const fileHash = 123; + const state = { + diffFiles: [{}, { content_sha: 'abc', file_hash: fileHash, existing_field: 0 }], + }; + const data = { + diff_files: [ + { + content_sha: 'abc', + file_hash: fileHash, + extra_field: 1, + existing_field: 1, + viewer: { name: 'text' }, + }, + ], + }; + + mutations[types.ADD_COLLAPSED_DIFFS](state, { file: state.diffFiles[1], data }); + + expect(state.diffFiles[1].file_hash).toEqual(fileHash); + expect(state.diffFiles[1].existing_field).toEqual(1); + expect(state.diffFiles[1].extra_field).toEqual(1); + }); + }); + + describe('SET_LINE_DISCUSSIONS_FOR_FILE', () => { + it('should add discussions to the given line', () => { + const diffPosition = { + base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + new_line: null, + new_path: '500-lines-4.txt', + old_line: 5, + old_path: '500-lines-4.txt', + start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1', + discussions: [], + }, + right: { + line_code: 'ABC_2', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [], + }, + ], + }, + ], + }; + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + const diffPositionByLineCode = { + ABC_1: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); + }); + + it('should not duplicate discussions on line', () => { + const diffPosition = { + base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + new_line: null, + new_path: '500-lines-4.txt', + old_line: 5, + old_path: '500-lines-4.txt', + start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1', + discussions: [], + }, + right: { + line_code: 'ABC_2', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [], + }, + ], + }, + ], + }; + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + const diffPositionByLineCode = { + ABC_1: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); + }); + + it('updates existing discussion', () => { + const diffPosition = { + base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + new_line: null, + new_path: '500-lines-4.txt', + old_line: 5, + old_path: '500-lines-4.txt', + start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1', + discussions: [], + }, + right: { + line_code: 'ABC_2', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [], + }, + ], + }, + ], + }; + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + const diffPositionByLineCode = { + ABC_1: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion: { + ...discussion, + resolved: true, + notes: ['test'], + }, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].notes.length).toBe(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].notes.length).toBe(1); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].resolved).toBe(true); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].resolved).toBe(true); + }); + + it('should not duplicate inline diff discussions', () => { + const diffPosition = { + base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + new_line: null, + new_path: '500-lines-4.txt', + old_line: 5, + old_path: '500-lines-4.txt', + start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [ + { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: 'ABC', + }, + }, + ], + }, + { + line_code: 'ABC_2', + discussions: [], + }, + ], + parallel_diff_lines: [], + }, + ], + }; + const discussion = { + id: 2, + line_code: 'ABC_2', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + const diffPositionByLineCode = { + ABC_2: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toBe(1); + }); + + it('should add legacy discussions to the given line', () => { + const diffPosition = { + base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + new_line: null, + new_path: '500-lines-4.txt', + old_line: 5, + old_path: '500-lines-4.txt', + start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + line_code: 'ABC_1', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1', + discussions: [], + }, + right: { + line_code: 'ABC_1', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [], + }, + ], + }, + ], + }; + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + active: true, + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + const diffPositionByLineCode = { + ABC_1: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); + }); + }); + + describe('REMOVE_LINE_DISCUSSIONS', () => { + it('should remove the existing discussions on the given line', () => { + const state = { + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1', + discussions: [ + { + id: 1, + line_code: 'ABC_1', + notes: [], + }, + { + id: 2, + line_code: 'ABC_1', + notes: [], + }, + ], + }, + right: { + line_code: 'ABC_1', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [ + { + id: 1, + line_code: 'ABC_1', + notes: [], + }, + { + id: 2, + line_code: 'ABC_1', + notes: [], + }, + ], + }, + ], + }, + ], + }; + + mutations[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { + fileHash: 'ABC', + lineCode: 'ABC_1', + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(0); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(0); + }); + }); + + describe('TOGGLE_FOLDER_OPEN', () => { + it('toggles entry opened prop', () => { + const state = { + treeEntries: { + path: { + opened: false, + }, + }, + }; + + mutations[types.TOGGLE_FOLDER_OPEN](state, 'path'); + + expect(state.treeEntries.path.opened).toBe(true); + }); + }); + + describe('TOGGLE_SHOW_TREE_LIST', () => { + it('toggles showTreeList', () => { + const state = createState(); + + mutations[types.TOGGLE_SHOW_TREE_LIST](state); + + expect(state.showTreeList).toBe(false, 'Failed to toggle showTreeList to false'); + + mutations[types.TOGGLE_SHOW_TREE_LIST](state); + + expect(state.showTreeList).toBe(true, 'Failed to toggle showTreeList to true'); + }); + }); + + describe('UPDATE_CURRENT_DIFF_FILE_ID', () => { + it('updates currentDiffFileId', () => { + const state = createState(); + + mutations[types.UPDATE_CURRENT_DIFF_FILE_ID](state, 'somefileid'); + + expect(state.currentDiffFileId).toBe('somefileid'); + }); + }); + + describe('Set highlighted row', () => { + it('sets highlighted row', () => { + const state = createState(); + + mutations[types.SET_HIGHLIGHTED_ROW](state, 'ABC_123'); + + expect(state.highlightedRow).toBe('ABC_123'); + }); + }); + + describe('TOGGLE_LINE_HAS_FORM', () => { + it('sets hasForm on lines', () => { + const file = { + file_hash: 'hash', + parallel_diff_lines: [ + { left: { line_code: '123', hasForm: false }, right: {} }, + { left: {}, right: { line_code: '124', hasForm: false } }, + ], + highlighted_diff_lines: [ + { line_code: '123', hasForm: false }, + { line_code: '124', hasForm: false }, + ], + }; + const state = { + diffFiles: [file], + }; + + mutations[types.TOGGLE_LINE_HAS_FORM](state, { + lineCode: '123', + hasForm: true, + fileHash: 'hash', + }); + + expect(file.highlighted_diff_lines[0].hasForm).toBe(true); + expect(file.highlighted_diff_lines[1].hasForm).toBe(false); + + expect(file.parallel_diff_lines[0].left.hasForm).toBe(true); + expect(file.parallel_diff_lines[1].right.hasForm).toBe(false); + }); + }); + + describe('SET_TREE_DATA', () => { + it('sets treeEntries and tree in state', () => { + const state = { + treeEntries: {}, + tree: [], + }; + + mutations[types.SET_TREE_DATA](state, { + treeEntries: { file: { name: 'index.js' } }, + tree: ['tree'], + }); + + expect(state.treeEntries).toEqual({ + file: { + name: 'index.js', + }, + }); + + expect(state.tree).toEqual(['tree']); + }); + }); + + describe('SET_RENDER_TREE_LIST', () => { + it('sets renderTreeList', () => { + const state = { + renderTreeList: true, + }; + + mutations[types.SET_RENDER_TREE_LIST](state, false); + + expect(state.renderTreeList).toBe(false); + }); + }); + + describe('SET_SHOW_WHITESPACE', () => { + it('sets showWhitespace', () => { + const state = { + showWhitespace: true, + }; + + mutations[types.SET_SHOW_WHITESPACE](state, false); + + expect(state.showWhitespace).toBe(false); + }); + }); + + describe('REQUEST_FULL_DIFF', () => { + it('sets isLoadingFullFile to true', () => { + const state = { + diffFiles: [{ file_path: 'test', isLoadingFullFile: false }], + }; + + mutations[types.REQUEST_FULL_DIFF](state, 'test'); + + expect(state.diffFiles[0].isLoadingFullFile).toBe(true); + }); + }); + + describe('RECEIVE_FULL_DIFF_ERROR', () => { + it('sets isLoadingFullFile to false', () => { + const state = { + diffFiles: [{ file_path: 'test', isLoadingFullFile: true }], + }; + + mutations[types.RECEIVE_FULL_DIFF_ERROR](state, 'test'); + + expect(state.diffFiles[0].isLoadingFullFile).toBe(false); + }); + }); + + describe('RECEIVE_FULL_DIFF_SUCCESS', () => { + it('sets isLoadingFullFile to false', () => { + const state = { + diffFiles: [ + { + file_path: 'test', + isLoadingFullFile: true, + isShowingFullFile: false, + highlighted_diff_lines: [], + parallel_diff_lines: [], + }, + ], + }; + + mutations[types.RECEIVE_FULL_DIFF_SUCCESS](state, { filePath: 'test', data: [] }); + + expect(state.diffFiles[0].isLoadingFullFile).toBe(false); + }); + + it('sets isShowingFullFile to true', () => { + const state = { + diffFiles: [ + { + file_path: 'test', + isLoadingFullFile: true, + isShowingFullFile: false, + highlighted_diff_lines: [], + parallel_diff_lines: [], + }, + ], + }; + + mutations[types.RECEIVE_FULL_DIFF_SUCCESS](state, { filePath: 'test', data: [] }); + + expect(state.diffFiles[0].isShowingFullFile).toBe(true); + }); + }); + + describe('SET_FILE_COLLAPSED', () => { + it('sets collapsed', () => { + const state = { + diffFiles: [{ file_path: 'test', viewer: { collapsed: false } }], + }; + + mutations[types.SET_FILE_COLLAPSED](state, { filePath: 'test', collapsed: true }); + + expect(state.diffFiles[0].viewer.collapsed).toBe(true); + }); + }); + + describe('SET_HIDDEN_VIEW_DIFF_FILE_LINES', () => { + [ + { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' }, + { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' }, + ].forEach(({ current, hidden, diffViewType }) => { + it(`sets the ${hidden} lines when diff view is ${diffViewType}`, () => { + const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] }; + const state = { + diffFiles: [file], + diffViewType, + }; + + mutations[types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, { + filePath: 'test', + lines: ['test'], + }); + + expect(file[`${current}_diff_lines`]).toEqual([]); + expect(file[`${hidden}_diff_lines`]).toEqual(['test']); + }); + }); + }); + + describe('SET_CURRENT_VIEW_DIFF_FILE_LINES', () => { + [ + { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' }, + { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' }, + ].forEach(({ current, hidden, diffViewType }) => { + it(`sets the ${current} lines when diff view is ${diffViewType}`, () => { + const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] }; + const state = { + diffFiles: [file], + diffViewType, + }; + + mutations[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { + filePath: 'test', + lines: ['test'], + }); + + expect(file[`${current}_diff_lines`]).toEqual(['test']); + expect(file[`${hidden}_diff_lines`]).toEqual([]); + }); + }); + }); + + describe('ADD_CURRENT_VIEW_DIFF_FILE_LINES', () => { + [ + { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' }, + { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' }, + ].forEach(({ current, hidden, diffViewType }) => { + it(`pushes to ${current} lines when diff view is ${diffViewType}`, () => { + const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] }; + const state = { + diffFiles: [file], + diffViewType, + }; + + mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, { + filePath: 'test', + line: 'test', + }); + + expect(file[`${current}_diff_lines`]).toEqual(['test']); + expect(file[`${hidden}_diff_lines`]).toEqual([]); + + mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, { + filePath: 'test', + line: 'test2', + }); + + expect(file[`${current}_diff_lines`]).toEqual(['test', 'test2']); + expect(file[`${hidden}_diff_lines`]).toEqual([]); + }); + }); + }); + + describe('TOGGLE_DIFF_FILE_RENDERING_MORE', () => { + it('toggles renderingLines on file', () => { + const file = { file_path: 'test', renderingLines: false }; + const state = { + diffFiles: [file], + }; + + mutations[types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, 'test'); + + expect(file.renderingLines).toBe(true); + + mutations[types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, 'test'); + + expect(file.renderingLines).toBe(false); + }); + }); + + describe('SET_SHOW_SUGGEST_POPOVER', () => { + it('sets showSuggestPopover to false', () => { + const state = { showSuggestPopover: true }; + + mutations[types.SET_SHOW_SUGGEST_POPOVER](state); + + expect(state.showSuggestPopover).toBe(false); + }); + }); +}); diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js new file mode 100644 index 00000000000..1adcdab272a --- /dev/null +++ b/spec/frontend/diffs/store/utils_spec.js @@ -0,0 +1,950 @@ +import { clone } from 'lodash'; +import * as utils from '~/diffs/store/utils'; +import { + LINE_POSITION_LEFT, + LINE_POSITION_RIGHT, + TEXT_DIFF_POSITION_TYPE, + LEGACY_DIFF_NOTE_TYPE, + DIFF_NOTE_TYPE, + NEW_LINE_TYPE, + OLD_LINE_TYPE, + MATCH_LINE_TYPE, + INLINE_DIFF_VIEW_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 = () => JSON.parse(JSON.stringify(diffFileMockData)); + +describe('DiffsStoreUtils', () => { + describe('findDiffFile', () => { + const files = [{ file_hash: 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().highlighted_diff_lines); + }); + }); + + describe('findIndexInParallelLines', () => { + it('should return correct index for given line numbers', () => { + expectSet(utils.findIndexInParallelLines, getDiffFileMock().parallel_diff_lines, []); + }); + }); + }); + + describe('getPreviousLineIndex', () => { + [ + { diffViewType: INLINE_DIFF_VIEW_TYPE, file: { parallel_diff_lines: [] } }, + { diffViewType: PARALLEL_DIFF_VIEW_TYPE, file: { highlighted_diff_lines: [] } }, + ].forEach(({ diffViewType, file }) => { + describe(`with diffViewType (${diffViewType}) in split diffs`, () => { + let diffFile; + + beforeEach(() => { + diffFile = { ...clone(diffFileMockData), ...file }; + }); + + it('should return the correct previous line number', () => { + const emptyLines = + diffViewType === INLINE_DIFF_VIEW_TYPE + ? diffFile.parallel_diff_lines + : diffFile.highlighted_diff_lines; + + // This expectation asserts that we cannot possibly be using the opposite view type lines in the next expectation + expect(emptyLines.length).toBe(0); + expect( + utils.getPreviousLineIndex(diffViewType, diffFile, { + oldLineNumber: 3, + newLineNumber: 5, + }), + ).toBe(4); + }); + }); + }); + }); + + 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.highlighted_diff_lines, + lineNumbers, + ); + const parallelIndex = utils.findIndexInParallelLines( + diffFile.parallel_diff_lines, + lineNumbers, + ); + const atInlineIndex = diffFile.highlighted_diff_lines[inlineIndex]; + const atParallelIndex = diffFile.parallel_diff_lines[parallelIndex]; + + utils.removeMatchLine(diffFile, lineNumbers, false); + + expect(diffFile.highlighted_diff_lines[inlineIndex]).not.toEqual(atInlineIndex); + expect(diffFile.parallel_diff_lines[parallelIndex]).not.toEqual(atParallelIndex); + + utils.removeMatchLine(diffFile, lineNumbers, true); + + expect(diffFile.highlighted_diff_lines[inlineIndex + 1]).not.toEqual(atInlineIndex); + expect(diffFile.parallel_diff_lines[parallelIndex + 1]).not.toEqual(atParallelIndex); + }); + }); + + describe('addContextLines', () => { + [INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE].forEach(diffViewType => { + it(`should add context lines for ${diffViewType}`, () => { + const diffFile = getDiffFileMock(); + const inlineLines = diffFile.highlighted_diff_lines; + const parallelLines = diffFile.parallel_diff_lines; + const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; + const contextLines = [{ lineNumber: 42, line_code: '123' }]; + const options = { inlineLines, parallelLines, contextLines, lineNumbers, diffViewType }; + const inlineIndex = utils.findIndexInInlineLines(inlineLines, lineNumbers); + const parallelIndex = utils.findIndexInParallelLines(parallelLines, lineNumbers); + const normalizedParallelLine = { + left: options.contextLines[0], + right: options.contextLines[0], + line_code: '123', + }; + + utils.addContextLines(options); + + if (diffViewType === INLINE_DIFF_VIEW_TYPE) { + expect(inlineLines[inlineIndex]).toEqual(contextLines[0]); + } else { + expect(parallelLines[parallelIndex]).toEqual(normalizedParallelLine); + } + }); + + it(`should add context lines properly with bottom parameter for ${diffViewType}`, () => { + const diffFile = getDiffFileMock(); + const inlineLines = diffFile.highlighted_diff_lines; + const parallelLines = diffFile.parallel_diff_lines; + const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; + const contextLines = [{ lineNumber: 42, line_code: '123' }]; + const options = { + inlineLines, + parallelLines, + contextLines, + lineNumbers, + bottom: true, + diffViewType, + }; + const normalizedParallelLine = { + left: options.contextLines[0], + right: options.contextLines[0], + line_code: '123', + }; + + utils.addContextLines(options); + + if (diffViewType === INLINE_DIFF_VIEW_TYPE) { + expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]); + } else { + expect(parallelLines[parallelLines.length - 1]).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: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + meta_data: null, + new_line: 3, + old_line: 1, + }, + diffViewType: PARALLEL_DIFF_VIEW_TYPE, + linePosition: LINE_POSITION_LEFT, + }; + + const position = JSON.stringify({ + base_sha: diffFile.diff_refs.base_sha, + start_sha: diffFile.diff_refs.start_sha, + head_sha: diffFile.diff_refs.head_sha, + old_path: diffFile.old_path, + new_path: diffFile.new_path, + position_type: TEXT_DIFF_POSITION_TYPE, + old_line: options.noteTargetLine.old_line, + new_line: options.noteTargetLine.new_line, + }); + + const postData = { + view: options.diffViewType, + line_type: options.linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE, + merge_request_diff_head_sha: diffFile.diff_refs.head_sha, + in_reply_to_discussion_id: '', + note_project_id: '', + target_type: options.noteableType, + target_id: options.noteableData.id, + return_discussion: true, + note: { + noteable_type: options.noteableType, + noteable_id: options.noteableData.id, + commit_id: undefined, + type: DIFF_NOTE_TYPE, + line_code: options.noteTargetLine.line_code, + note: options.note, + position, + }, + }; + + expect(utils.getNoteFormData(options)).toEqual({ + endpoint: options.noteableData.create_note_path, + data: postData, + }); + }); + + it('should create legacy note form data', () => { + const diffFile = getDiffFileMock(); + delete diffFile.diff_refs.start_sha; + delete diffFile.diff_refs.head_sha; + + noteableDataMock.targetType = MERGE_REQUEST_NOTEABLE_TYPE; + + const options = { + note: 'Hello world!', + noteableData: noteableDataMock, + noteableType: MERGE_REQUEST_NOTEABLE_TYPE, + diffFile, + noteTargetLine: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + meta_data: null, + new_line: 3, + old_line: 1, + }, + diffViewType: PARALLEL_DIFF_VIEW_TYPE, + linePosition: LINE_POSITION_LEFT, + }; + + const position = JSON.stringify({ + base_sha: diffFile.diff_refs.base_sha, + start_sha: undefined, + head_sha: undefined, + old_path: diffFile.old_path, + new_path: diffFile.new_path, + position_type: TEXT_DIFF_POSITION_TYPE, + old_line: options.noteTargetLine.old_line, + new_line: options.noteTargetLine.new_line, + }); + + const postData = { + view: options.diffViewType, + line_type: options.linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE, + merge_request_diff_head_sha: undefined, + in_reply_to_discussion_id: '', + note_project_id: '', + target_type: options.noteableType, + target_id: options.noteableData.id, + return_discussion: true, + note: { + noteable_type: options.noteableType, + noteable_id: options.noteableData.id, + commit_id: undefined, + type: LEGACY_DIFF_NOTE_TYPE, + line_code: options.noteTargetLine.line_code, + 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].old_line).toEqual(lineNumbers.oldLineNumber + 1); + expect(linesWithReferences[0].new_line).toEqual(lineNumbers.newLineNumber + 1); + expect(linesWithReferences[1].meta_data.old_pos).toEqual(4); + expect(linesWithReferences[1].meta_data.new_pos).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].old_line).toEqual(0); + expect(linesWithReferences[0].new_line).toEqual(1); + expect(linesWithReferences[1].meta_data.old_pos).toEqual(2); + expect(linesWithReferences[1].meta_data.new_pos).toEqual(3); + }); + + it('should add correct line references when isExpandDown is true', () => { + const lines = [{ type: null }, { type: MATCH_LINE_TYPE }]; + const linesWithReferences = utils.addLineReferences(lines, lineNumbers, false, true, { + old_line: 10, + new_line: 11, + }); + + expect(linesWithReferences[1].meta_data.old_pos).toEqual(10); + expect(linesWithReferences[1].meta_data.new_pos).toEqual(11); + }); + }); + + describe('trimFirstCharOfLineContent', () => { + it('trims the line when it starts with a space', () => { + expect(utils.trimFirstCharOfLineContent({ rich_text: ' diff' })).toEqual({ + rich_text: 'diff', + }); + }); + + it('trims the line when it starts with a +', () => { + expect(utils.trimFirstCharOfLineContent({ rich_text: '+diff' })).toEqual({ + rich_text: 'diff', + }); + }); + + it('trims the line when it starts with a -', () => { + expect(utils.trimFirstCharOfLineContent({ rich_text: '-diff' })).toEqual({ + rich_text: 'diff', + }); + }); + + it('does not trims the line when it starts with a letter', () => { + expect(utils.trimFirstCharOfLineContent({ rich_text: 'diff' })).toEqual({ + rich_text: 'diff', + }); + }); + + it('does not modify the provided object', () => { + const lineObj = { + rich_text: ' diff', + }; + + utils.trimFirstCharOfLineContent(lineObj); + + expect(lineObj).toEqual({ rich_text: ' diff' }); + }); + + it('handles a undefined or null parameter', () => { + expect(utils.trimFirstCharOfLineContent()).toEqual({}); + }); + }); + + describe('prepareDiffData', () => { + let mock; + let preparedDiff; + let splitInlineDiff; + let splitParallelDiff; + let completedDiff; + + beforeEach(() => { + mock = getDiffFileMock(); + preparedDiff = { diff_files: [mock] }; + splitInlineDiff = { + diff_files: [Object.assign({}, mock, { parallel_diff_lines: undefined })], + }; + splitParallelDiff = { + diff_files: [Object.assign({}, mock, { highlighted_diff_lines: undefined })], + }; + completedDiff = { + diff_files: [Object.assign({}, mock, { highlighted_diff_lines: undefined })], + }; + + preparedDiff.diff_files = utils.prepareDiffData(preparedDiff); + splitInlineDiff.diff_files = utils.prepareDiffData(splitInlineDiff); + splitParallelDiff.diff_files = utils.prepareDiffData(splitParallelDiff); + completedDiff.diff_files = utils.prepareDiffData(completedDiff, [mock]); + }); + + it('sets the renderIt and collapsed attribute on files', () => { + const firstParallelDiffLine = preparedDiff.diff_files[0].parallel_diff_lines[2]; + + expect(firstParallelDiffLine.left.discussions.length).toBe(0); + expect(firstParallelDiffLine.left).not.toHaveAttr('text'); + expect(firstParallelDiffLine.right.discussions.length).toBe(0); + expect(firstParallelDiffLine.right).not.toHaveAttr('text'); + const firstParallelChar = firstParallelDiffLine.right.rich_text.charAt(0); + + expect(firstParallelChar).not.toBe(' '); + expect(firstParallelChar).not.toBe('+'); + expect(firstParallelChar).not.toBe('-'); + + const checkLine = preparedDiff.diff_files[0].highlighted_diff_lines[0]; + + expect(checkLine.discussions.length).toBe(0); + expect(checkLine).not.toHaveAttr('text'); + const firstChar = checkLine.rich_text.charAt(0); + + expect(firstChar).not.toBe(' '); + expect(firstChar).not.toBe('+'); + expect(firstChar).not.toBe('-'); + + expect(preparedDiff.diff_files[0].renderIt).toBeTruthy(); + expect(preparedDiff.diff_files[0].collapsed).toBeFalsy(); + }); + + it('adds line_code to all lines', () => { + expect( + preparedDiff.diff_files[0].parallel_diff_lines.filter(line => !line.line_code), + ).toHaveLength(0); + }); + + it('uses right line code if left has none', () => { + const firstLine = preparedDiff.diff_files[0].parallel_diff_lines[0]; + + expect(firstLine.line_code).toEqual(firstLine.right.line_code); + }); + + it('guarantees an empty array for both diff styles', () => { + expect(splitInlineDiff.diff_files[0].parallel_diff_lines.length).toEqual(0); + expect(splitInlineDiff.diff_files[0].highlighted_diff_lines.length).toBeGreaterThan(0); + expect(splitParallelDiff.diff_files[0].parallel_diff_lines.length).toBeGreaterThan(0); + expect(splitParallelDiff.diff_files[0].highlighted_diff_lines.length).toEqual(0); + }); + + it('merges existing diff files with newly loaded diff files to ensure split diffs are eventually completed', () => { + expect(completedDiff.diff_files.length).toEqual(1); + expect(completedDiff.diff_files[0].parallel_diff_lines.length).toBeGreaterThan(0); + expect(completedDiff.diff_files[0].highlighted_diff_lines.length).toBeGreaterThan(0); + }); + + it('leaves files in the existing state', () => { + const priorFiles = [mock]; + const fakeNewFile = { + ...mock, + content_sha: 'ABC', + file_hash: 'DEF', + }; + const updatedFilesList = utils.prepareDiffData({ diff_files: [fakeNewFile] }, priorFiles); + + expect(updatedFilesList).toEqual([mock, fakeNewFile]); + }); + + it('completes an existing split diff without overwriting existing diffs', () => { + // The current state has a file that has only loaded inline lines + const priorFiles = [{ ...mock, parallel_diff_lines: [] }]; + // The next (batch) load loads two files: the other half of that file, and a new file + const fakeBatch = [ + { ...mock, highlighted_diff_lines: undefined }, + { ...mock, highlighted_diff_lines: undefined, content_sha: 'ABC', file_hash: 'DEF' }, + ]; + const updatedFilesList = utils.prepareDiffData({ diff_files: fakeBatch }, priorFiles); + + expect(updatedFilesList).toEqual([ + mock, + expect.objectContaining({ + content_sha: 'ABC', + file_hash: 'DEF', + }), + ]); + }); + }); + + describe('isDiscussionApplicableToLine', () => { + const diffPosition = { + baseSha: 'ed13df29948c41ba367caa757ab3ec4892509910', + headSha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + newLine: null, + newPath: '500-lines-4.txt', + oldLine: 5, + oldPath: '500-lines-4.txt', + startSha: 'ed13df29948c41ba367caa757ab3ec4892509910', + }; + + const wrongDiffPosition = { + baseSha: 'wrong', + headSha: 'wrong', + newLine: null, + newPath: '500-lines-4.txt', + oldLine: 5, + oldPath: '500-lines-4.txt', + startSha: 'wrong', + }; + + const discussions = { + upToDateDiscussion1: { + original_position: diffPosition, + position: wrongDiffPosition, + }, + outDatedDiscussion1: { + original_position: wrongDiffPosition, + position: wrongDiffPosition, + }, + }; + + it('returns true when the discussion is up to date', () => { + expect( + utils.isDiscussionApplicableToLine({ + discussion: discussions.upToDateDiscussion1, + diffPosition, + latestDiff: true, + }), + ).toBe(true); + }); + + it('returns false when the discussion is not up to date', () => { + expect( + utils.isDiscussionApplicableToLine({ + discussion: discussions.outDatedDiscussion1, + diffPosition, + latestDiff: true, + }), + ).toBe(false); + }); + + it('returns true when line codes match and discussion does not contain position and is not active', () => { + const discussion = { ...discussions.outDatedDiscussion1, line_code: 'ABC_1', active: false }; + delete discussion.original_position; + delete discussion.position; + + expect( + utils.isDiscussionApplicableToLine({ + discussion, + diffPosition: { + ...diffPosition, + lineCode: 'ABC_1', + }, + latestDiff: true, + }), + ).toBe(false); + }); + + it('returns true when line codes match and discussion does not contain position and is active', () => { + const discussion = { ...discussions.outDatedDiscussion1, line_code: 'ABC_1', active: true }; + delete discussion.original_position; + delete discussion.position; + + expect( + utils.isDiscussionApplicableToLine({ + discussion, + diffPosition: { + ...diffPosition, + line_code: 'ABC_1', + }, + latestDiff: true, + }), + ).toBe(true); + }); + + it('returns false when not latest diff', () => { + const discussion = { ...discussions.outDatedDiscussion1, line_code: 'ABC_1', active: true }; + delete discussion.original_position; + delete discussion.position; + + expect( + utils.isDiscussionApplicableToLine({ + discussion, + diffPosition: { + ...diffPosition, + lineCode: 'ABC_1', + }, + latestDiff: false, + }), + ).toBe(false); + }); + }); + + describe('generateTreeList', () => { + let files; + + beforeAll(() => { + files = [ + { + new_path: 'app/index.js', + deleted_file: false, + new_file: false, + removed_lines: 10, + added_lines: 0, + file_hash: 'test', + }, + { + new_path: 'app/test/index.js', + deleted_file: false, + new_file: true, + removed_lines: 0, + added_lines: 0, + file_hash: 'test', + }, + { + new_path: 'app/test/filepathneedstruncating.js', + deleted_file: false, + new_file: true, + removed_lines: 0, + added_lines: 0, + file_hash: 'test', + }, + { + new_path: 'package.json', + deleted_file: true, + new_file: false, + removed_lines: 0, + added_lines: 0, + file_hash: 'test', + }, + ]; + }); + + it('creates a tree of files', () => { + const { tree } = utils.generateTreeList(files); + + expect(tree).toEqual([ + { + key: 'app', + path: 'app', + name: 'app', + type: 'tree', + tree: [ + { + addedLines: 0, + changed: true, + deleted: false, + fileHash: 'test', + key: 'app/index.js', + name: 'index.js', + parentPath: 'app/', + path: 'app/index.js', + removedLines: 10, + tempFile: false, + type: 'blob', + tree: [], + }, + { + key: 'app/test', + path: 'app/test', + name: 'test', + type: 'tree', + opened: true, + tree: [ + { + addedLines: 0, + changed: true, + deleted: false, + fileHash: 'test', + key: 'app/test/index.js', + name: 'index.js', + parentPath: 'app/test/', + path: 'app/test/index.js', + removedLines: 0, + tempFile: true, + type: 'blob', + tree: [], + }, + { + addedLines: 0, + changed: true, + deleted: false, + fileHash: 'test', + key: 'app/test/filepathneedstruncating.js', + name: 'filepathneedstruncating.js', + parentPath: 'app/test/', + path: 'app/test/filepathneedstruncating.js', + removedLines: 0, + tempFile: true, + type: 'blob', + tree: [], + }, + ], + }, + ], + opened: true, + }, + { + key: 'package.json', + parentPath: '/', + path: 'package.json', + name: 'package.json', + type: 'blob', + changed: true, + tempFile: false, + deleted: true, + fileHash: 'test', + addedLines: 0, + removedLines: 0, + tree: [], + }, + ]); + }); + + it('creates flat list of blobs & folders', () => { + const { treeEntries } = utils.generateTreeList(files); + + expect(Object.keys(treeEntries)).toEqual([ + 'app', + 'app/index.js', + 'app/test', + 'app/test/index.js', + 'app/test/filepathneedstruncating.js', + 'package.json', + ]); + }); + }); + + describe('getDiffMode', () => { + it('returns mode when matched in file', () => { + expect( + utils.getDiffMode({ + renamed_file: true, + }), + ).toBe('renamed'); + }); + + it('returns mode_changed if key has no match', () => { + expect( + utils.getDiffMode({ + viewer: { name: 'mode_changed' }, + }), + ).toBe('mode_changed'); + }); + + it('defaults to replaced', () => { + expect(utils.getDiffMode({})).toBe('replaced'); + }); + }); + + describe('getLowestSingleFolder', () => { + it('returns path and tree of lowest single folder tree', () => { + const folder = { + name: 'app', + type: 'tree', + tree: [ + { + name: 'javascripts', + type: 'tree', + tree: [ + { + type: 'blob', + name: 'index.js', + }, + ], + }, + ], + }; + const { path, treeAcc } = utils.getLowestSingleFolder(folder); + + expect(path).toEqual('app/javascripts'); + expect(treeAcc).toEqual([ + { + type: 'blob', + name: 'index.js', + }, + ]); + }); + + it('returns passed in folders path & tree when more than tree exists', () => { + const folder = { + name: 'app', + type: 'tree', + tree: [ + { + name: 'spec', + type: 'blob', + tree: [], + }, + ], + }; + const { path, treeAcc } = utils.getLowestSingleFolder(folder); + + expect(path).toEqual('app'); + expect(treeAcc).toBeNull(); + }); + }); + + describe('flattenTree', () => { + it('returns flattened directory structure', () => { + const tree = [ + { + type: 'tree', + name: 'app', + tree: [ + { + type: 'tree', + name: 'javascripts', + tree: [ + { + type: 'blob', + name: 'index.js', + tree: [], + }, + ], + }, + ], + }, + { + type: 'tree', + name: 'ee', + tree: [ + { + type: 'tree', + name: 'lib', + tree: [ + { + type: 'tree', + name: 'ee', + tree: [ + { + type: 'tree', + name: 'gitlab', + tree: [ + { + type: 'tree', + name: 'checks', + tree: [ + { + type: 'tree', + name: 'longtreenametomakepath', + tree: [ + { + type: 'blob', + name: 'diff_check.rb', + tree: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + type: 'tree', + name: 'spec', + tree: [ + { + type: 'tree', + name: 'javascripts', + tree: [], + }, + { + type: 'blob', + name: 'index_spec.js', + tree: [], + }, + ], + }, + ]; + const flattened = utils.flattenTree(tree); + + expect(flattened).toEqual([ + { + type: 'tree', + name: 'app/javascripts', + tree: [ + { + type: 'blob', + name: 'index.js', + tree: [], + }, + ], + }, + { + type: 'tree', + name: 'ee/lib/…/…/…/longtreenametomakepath', + tree: [ + { + name: 'diff_check.rb', + tree: [], + type: 'blob', + }, + ], + }, + { + type: 'tree', + name: 'spec', + tree: [ + { + type: 'tree', + name: 'javascripts', + tree: [], + }, + { + type: 'blob', + name: 'index_spec.js', + tree: [], + }, + ], + }, + ]); + }); + }); + + describe('convertExpandLines', () => { + it('converts expanded lines to normal lines', () => { + const diffLines = [ + { + type: 'match', + old_line: 1, + new_line: 1, + }, + { + type: '', + old_line: 2, + new_line: 2, + }, + ]; + + const lines = utils.convertExpandLines({ + diffLines, + data: [{ text: 'expanded' }], + typeKey: 'type', + oldLineKey: 'old_line', + newLineKey: 'new_line', + mapLine: ({ line, oldLine, newLine }) => ({ + ...line, + old_line: oldLine, + new_line: newLine, + }), + }); + + expect(lines).toEqual([ + { + text: 'expanded', + new_line: 1, + old_line: 1, + discussions: [], + hasForm: false, + }, + { + type: '', + old_line: 2, + new_line: 2, + }, + ]); + }); + }); +}); |