import MockAdapter from 'axios-mock-adapter'; import Cookies from 'js-cookie'; import { DIFF_VIEW_COOKIE_NAME, INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE, } from '~/diffs/constants'; import actions, { setBaseConfig, fetchDiffFiles, assignDiscussionsToDiff, removeDiscussionsFromDiff, startRenderDiffsQueue, setInlineDiffViewType, setParallelDiffViewType, showCommentForm, cancelCommentForm, loadMoreLines, scrollToLineIfNeededInline, scrollToLineIfNeededParallel, loadCollapsedDiff, expandAllFiles, toggleFileDiscussions, saveDiffDiscussion, setHighlightedRow, toggleTreeOpen, scrollToFile, toggleShowTreeList, renderFileForDiscussionId, setRenderTreeList, setShowWhitespace, } 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 mockDiffFile from 'spec/diffs/mock_data/diff_file'; import testAction from '../../helpers/vuex_action_helper'; describe('DiffsStoreActions', () => { const originalMethods = { requestAnimationFrame: global.requestAnimationFrame, requestIdleCallback: global.requestIdleCallback, }; beforeEach(() => { ['requestAnimationFrame', 'requestIdleCallback'].forEach(method => { global[method] = cb => { cb(); }; }); }); afterEach(() => { ['requestAnimationFrame', 'requestIdleCallback'].forEach(method => { global[method] = originalMethods[method]; }); }); describe('setBaseConfig', () => { it('should set given endpoint and project path', done => { const endpoint = '/diffs/set/endpoint'; const projectPath = '/root/project'; testAction( setBaseConfig, { endpoint, projectPath }, { endpoint: '', projectPath: '' }, [{ type: types.SET_BASE_CONFIG, payload: { endpoint, projectPath } }], [], done, ); }); }); describe('fetchDiffFiles', () => { it('should fetch diff files', done => { const endpoint = '/fetch/diff/files'; const mock = new MockAdapter(axios); const res = { diff_files: 1, merge_request_diffs: [] }; mock.onGet(endpoint).reply(200, res); testAction( fetchDiffFiles, {}, { endpoint }, [ { type: types.SET_LOADING, payload: true }, { type: types.SET_LOADING, payload: false }, { type: types.SET_MERGE_REQUEST_DIFFS, payload: res.merge_request_diffs }, { type: types.SET_DIFF_DATA, payload: res }, ], [], () => { mock.restore(); done(); }, ); }); }); describe('setHighlightedRow', () => { it('should set lineHash and fileHash of highlightedRow', () => { testAction(setHighlightedRow, 'ABC_123', {}, [ { type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' }, ]); }); }); describe('assignDiscussionsToDiff', () => { it('should merge discussions into diffs', done => { 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', }, }, }, }, ], [], 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, collapsed: false, }, { id: 2, renderIt: false, 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 }], [], () => { setTimeout(() => { expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE); done(); }, 0); }, ); }); }); describe('setParallelDiffViewType', () => { it('should set diff view type to parallel and also set the cookie properly', done => { testAction( setParallelDiffViewType, null, {}, [{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }], [], () => { setTimeout(() => { expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE); done(); }, 0); }, ); }); }); describe('showCommentForm', () => { it('should call mutation to show comment form', done => { const payload = { lineCode: 'lineCode', 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 options = { endpoint, params, lineNumbers, fileHash }; 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 }, }, ], [], () => { mock.restore(); done(); }, ); }); }); describe('loadCollapsedDiff', () => { 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 = jasmine.createSpy('commit'); mock.onGet(file.loadCollapsedDiffUrl).reply(200, data); loadCollapsedDiff({ commit, getters: { commitId: null } }, 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, }; spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); loadCollapsedDiff({ commit() {}, getters }, file); expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, { params: { commit_id: null }, }); }); it('should fetch data with commit ID', () => { const file = { load_collapsed_diff_url: '/load/collapsed/diff/url' }; const getters = { commitId: '123', }; spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); loadCollapsedDiff({ commit() {}, getters }, file); expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, { params: { commit_id: '123' }, }); }); }); 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: jasmine.createSpy().and.returnValue([{ id: 1 }]), diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(true), diffHasAllCollapsedDiscussions: jasmine.createSpy().and.returnValue(false), }; const dispatch = jasmine.createSpy('dispatch'); toggleFileDiscussions({ getters, dispatch }); expect(dispatch).toHaveBeenCalledWith( 'collapseDiscussion', { discussionId: 1 }, { root: true }, ); }); it('should dispatch expandDiscussion when all discussions are collapsed', () => { const getters = { getDiffFileDiscussions: jasmine.createSpy().and.returnValue([{ id: 1 }]), diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(false), diffHasAllCollapsedDiscussions: jasmine.createSpy().and.returnValue(true), }; const dispatch = jasmine.createSpy(); 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: jasmine.createSpy().and.returnValue([{ expanded: false, id: 1 }]), diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(false), diffHasAllCollapsedDiscussions: jasmine.createSpy().and.returnValue(false), }; const dispatch = jasmine.createSpy(); 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 = ''; const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); scrollToLineIfNeededInline({}, lineMock); expect(handleLocationHashSpy).not.toHaveBeenCalled(); }); it('should not call handleLocationHash when the hash does not match any line', () => { window.location.hash = 'XYZ_456'; const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); scrollToLineIfNeededInline({}, lineMock); expect(handleLocationHashSpy).not.toHaveBeenCalled(); }); it('should call handleLocationHash only when the hash matches a line', () => { window.location.hash = 'ABC_123'; const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); scrollToLineIfNeededInline( {}, { lineCode: 'ABC_456', }, ); scrollToLineIfNeededInline({}, lineMock); scrollToLineIfNeededInline( {}, { lineCode: 'XYZ_456', }, ); expect(handleLocationHashSpy).toHaveBeenCalled(); expect(handleLocationHashSpy).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 = ''; const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); scrollToLineIfNeededParallel({}, lineMock); expect(handleLocationHashSpy).not.toHaveBeenCalled(); }); it('should not call handleLocationHash when the hash does not match any line', () => { window.location.hash = 'XYZ_456'; const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); scrollToLineIfNeededParallel({}, lineMock); expect(handleLocationHashSpy).not.toHaveBeenCalled(); }); it('should call handleLocationHash only when the hash matches a line', () => { window.location.hash = 'ABC_123'; const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); scrollToLineIfNeededParallel( {}, { left: null, right: { lineCode: 'ABC_456', }, }, ); scrollToLineIfNeededParallel({}, lineMock); scrollToLineIfNeededParallel( {}, { left: null, right: { lineCode: 'XYZ_456', }, }, ); expect(handleLocationHashSpy).toHaveBeenCalled(); expect(handleLocationHashSpy).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 = jasmine.createSpy('dispatch').and.callFake(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(() => { const { calls } = dispatch; expect(calls.count()).toBe(5); expect(calls.argsFor(0)).toEqual(['saveNote', jasmine.any(Object), { root: true }]); const postData = calls.argsFor(0)[1]; expect(postData.data.note.commit_id).toBe(commitId); expect(calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]); expect(calls.argsFor(2)).toEqual(['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 = jasmine.createSpy(); jasmine.clock().install(); }); afterEach(() => { jasmine.clock().uninstall(); }); 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'); }); it('resets currentDiffId after timeout', () => { const state = { treeEntries: { path: { fileHash: 'test', }, }, }; scrollToFile({ state, commit }, 'path'); jasmine.clock().tick(1000); expect(commit.calls.argsFor(1)).toEqual([types.UPDATE_CURRENT_DIFF_FILE_ID, '']); }); }); describe('toggleShowTreeList', () => { it('commits toggle', done => { testAction(toggleShowTreeList, null, {}, [{ type: types.TOGGLE_SHOW_TREE_LIST }], [], done); }); it('updates localStorage', () => { spyOn(localStorage, 'setItem'); toggleShowTreeList({ commit() {}, state: { showTreeList: true } }); expect(localStorage.setItem).toHaveBeenCalledWith('mr_tree_show', true); }); }); describe('renderFileForDiscussionId', () => { const rootState = { notes: { discussions: [ { id: '123', diff_file: { file_hash: 'HASH', }, }, { id: '456', diff_file: { file_hash: 'HASH', }, }, ], }, }; let commit; let $emit; let scrollToElement; const state = ({ collapsed, renderIt }) => ({ diffFiles: [ { file_hash: 'HASH', collapsed, renderIt, }, ], }); beforeEach(() => { commit = jasmine.createSpy('commit'); scrollToElement = spyOnDependency(actions, 'scrollToElement').and.stub(); $emit = 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(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(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', () => { spyOn(localStorage, 'setItem').and.stub(); setRenderTreeList({ commit() {} }, true); expect(localStorage.setItem).toHaveBeenCalledWith('mr_diff_tree_list', true); }); }); describe('setShowWhitespace', () => { it('commits SET_SHOW_WHITESPACE', done => { testAction( setShowWhitespace, { showWhitespace: true }, {}, [{ type: types.SET_SHOW_WHITESPACE, payload: true }], [], done, ); }); it('sets localStorage', () => { spyOn(localStorage, 'setItem').and.stub(); setShowWhitespace({ commit() {} }, { showWhitespace: true }); expect(localStorage.setItem).toHaveBeenCalledWith('mr_show_whitespace', true); }); it('calls history pushState', () => { spyOn(localStorage, 'setItem').and.stub(); spyOn(window.history, 'pushState').and.stub(); setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true }); expect(window.history.pushState).toHaveBeenCalled(); }); }); });