summaryrefslogtreecommitdiff
path: root/spec/frontend/diffs
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-20 15:09:17 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-20 15:09:17 +0000
commit194b499aa8e26df26ff70a1e1ce0396587bd5243 (patch)
treec873ac9c3096faf4a5da43d6670107461da2a7d7 /spec/frontend/diffs
parent43b4b3e2d2ddebc0a89b94a8251c162ec5719780 (diff)
downloadgitlab-ce-194b499aa8e26df26ff70a1e1ce0396587bd5243.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/diffs')
-rw-r--r--spec/frontend/diffs/components/commit_widget_spec.js19
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js102
-rw-r--r--spec/frontend/diffs/components/diff_expansion_cell_spec.js229
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js259
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js108
-rw-r--r--spec/frontend/diffs/components/file_row_stats_spec.js33
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js140
-rw-r--r--spec/frontend/diffs/components/inline_diff_expansion_row_spec.js31
-rw-r--r--spec/frontend/diffs/components/inline_diff_table_row_spec.js103
-rw-r--r--spec/frontend/diffs/components/inline_diff_view_spec.js54
-rw-r--r--spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js31
-rw-r--r--spec/frontend/diffs/components/parallel_diff_table_row_spec.js147
-rw-r--r--spec/frontend/diffs/components/parallel_diff_view_spec.js33
-rw-r--r--spec/frontend/diffs/mock_data/diff_file_unreadable.js244
-rw-r--r--spec/frontend/diffs/store/actions_spec.js1349
-rw-r--r--spec/frontend/diffs/store/getters_spec.js315
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js966
-rw-r--r--spec/frontend/diffs/store/utils_spec.js950
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,
+ },
+ ]);
+ });
+ });
+});