diff options
Diffstat (limited to 'spec/frontend/diffs')
-rw-r--r-- | spec/frontend/diffs/components/app_spec.js | 723 | ||||
-rw-r--r-- | spec/frontend/diffs/create_diffs_store.js | 15 |
2 files changed, 738 insertions, 0 deletions
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js new file mode 100644 index 00000000000..15f91871437 --- /dev/null +++ b/spec/frontend/diffs/components/app_spec.js @@ -0,0 +1,723 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'spec/test_constants'; +import Mousetrap from 'mousetrap'; +import App from '~/diffs/components/app.vue'; +import NoChanges from '~/diffs/components/no_changes.vue'; +import DiffFile from '~/diffs/components/diff_file.vue'; +import CompareVersions from '~/diffs/components/compare_versions.vue'; +import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue'; +import CommitWidget from '~/diffs/components/commit_widget.vue'; +import TreeList from '~/diffs/components/tree_list.vue'; +import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants'; +import createDiffsStore from '../create_diffs_store'; +import axios from '~/lib/utils/axios_utils'; +import diffsMockData from '../mock_data/merge_request_diffs'; + +const mergeRequestDiff = { version_index: 1 }; +const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`; + +describe('diffs/components/app', () => { + const oldMrTabs = window.mrTabs; + let store; + let wrapper; + let mock; + + function createComponent(props = {}, extendStore = () => {}) { + const localVue = createLocalVue(); + + localVue.use(Vuex); + + store = createDiffsStore(); + store.state.diffs.isLoading = false; + + extendStore(store); + + wrapper = shallowMount(localVue.extend(App), { + localVue, + propsData: { + endpoint: TEST_ENDPOINT, + endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`, + endpointBatch: `${TEST_HOST}/diff/endpointBatch`, + projectPath: 'namespace/project', + currentUser: {}, + changesEmptyStateIllustration: '', + dismissEndpoint: '', + showSuggestPopover: true, + ...props, + }, + store, + methods: { + isLatestVersion() { + return true; + }, + }, + }); + } + + function getOppositeViewType(currentViewType) { + return currentViewType === INLINE_DIFF_VIEW_TYPE + ? PARALLEL_DIFF_VIEW_TYPE + : INLINE_DIFF_VIEW_TYPE; + } + + beforeEach(() => { + // setup globals (needed for component to mount :/) + window.mrTabs = { + resetViewContainer: jest.fn(), + }; + window.mrTabs.expandViewContainer = jest.fn(); + mock = new MockAdapter(axios); + mock.onGet(TEST_ENDPOINT).reply(200, {}); + }); + + afterEach(() => { + // reset globals + window.mrTabs = oldMrTabs; + + // reset component + wrapper.destroy(); + + mock.restore(); + }); + + describe('fetch diff methods', () => { + beforeEach(done => { + const fetchResolver = () => { + store.state.diffs.retrievingBatches = false; + store.state.notes.discussions = 'test'; + return Promise.resolve({ real_size: 100 }); + }; + jest.spyOn(window, 'requestIdleCallback').mockImplementation(fn => fn()); + createComponent(); + jest.spyOn(wrapper.vm, 'fetchDiffFiles').mockImplementation(fetchResolver); + jest.spyOn(wrapper.vm, 'fetchDiffFilesMeta').mockImplementation(fetchResolver); + jest.spyOn(wrapper.vm, 'fetchDiffFilesBatch').mockImplementation(fetchResolver); + jest.spyOn(wrapper.vm, 'setDiscussions').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'startRenderDiffsQueue').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'unwatchDiscussions').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'unwatchRetrievingBatches').mockImplementation(() => {}); + store.state.diffs.retrievingBatches = true; + store.state.diffs.diffFiles = []; + wrapper.vm.$nextTick(done); + }); + + describe('when the diff view type changes and it should load a single diff view style', () => { + const noLinesDiff = { + highlighted_diff_lines: [], + parallel_diff_lines: [], + }; + const parallelLinesDiff = { + highlighted_diff_lines: [], + parallel_diff_lines: ['line'], + }; + const inlineLinesDiff = { + highlighted_diff_lines: ['line'], + parallel_diff_lines: [], + }; + const fullDiff = { + highlighted_diff_lines: ['line'], + parallel_diff_lines: ['line'], + }; + + function expectFetchToOccur({ + vueInstance, + done = () => {}, + batch = false, + existingFiles = 1, + } = {}) { + vueInstance.$nextTick(() => { + expect(vueInstance.diffFiles.length).toEqual(existingFiles); + + if (!batch) { + expect(vueInstance.fetchDiffFiles).toHaveBeenCalled(); + expect(vueInstance.fetchDiffFilesBatch).not.toHaveBeenCalled(); + } else { + expect(vueInstance.fetchDiffFiles).not.toHaveBeenCalled(); + expect(vueInstance.fetchDiffFilesBatch).toHaveBeenCalled(); + } + + done(); + }); + } + + beforeEach(() => { + wrapper.vm.glFeatures.singleMrDiffView = true; + }); + + it('fetches diffs if it has none', done => { + wrapper.vm.isLatestVersion = () => false; + + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, batch: false, existingFiles: 0, done }); + }); + + it('fetches diffs if it has both view styles, but no lines in either', done => { + wrapper.vm.isLatestVersion = () => false; + + store.state.diffs.diffFiles.push(noLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, done }); + }); + + it('fetches diffs if it only has inline view style', done => { + wrapper.vm.isLatestVersion = () => false; + + store.state.diffs.diffFiles.push(inlineLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, done }); + }); + + it('fetches diffs if it only has parallel view style', done => { + wrapper.vm.isLatestVersion = () => false; + + store.state.diffs.diffFiles.push(parallelLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, done }); + }); + + it('fetches batch diffs if it has none', done => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, existingFiles: 0, done }); + }); + + it('fetches batch diffs if it has both view styles, but no lines in either', done => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + + store.state.diffs.diffFiles.push(noLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done }); + }); + + it('fetches batch diffs if it only has inline view style', done => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + + store.state.diffs.diffFiles.push(inlineLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done }); + }); + + it('fetches batch diffs if it only has parallel view style', done => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + + store.state.diffs.diffFiles.push(parallelLinesDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done }); + }); + + it('does not fetch diffs if it has already fetched both styles of diff', () => { + wrapper.vm.glFeatures.diffsBatchLoad = false; + + store.state.diffs.diffFiles.push(fullDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expect(wrapper.vm.diffFiles.length).toEqual(1); + expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); + }); + + it('does not fetch batch diffs if it has already fetched both styles of diff', () => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + + store.state.diffs.diffFiles.push(fullDiff); + store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType); + + expect(wrapper.vm.diffFiles.length).toEqual(1); + expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); + }); + }); + + it('calls fetchDiffFiles if diffsBatchLoad is not enabled', done => { + expect(wrapper.vm.diffFilesLength).toEqual(0); + wrapper.vm.glFeatures.diffsBatchLoad = false; + wrapper.vm.fetchData(false); + + expect(wrapper.vm.fetchDiffFiles).toHaveBeenCalled(); + setImmediate(() => { + expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); + expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled(); + expect(wrapper.vm.diffFilesLength).toEqual(100); + expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled(); + + done(); + }); + }); + + it('calls batch methods if diffsBatchLoad is enabled, and not latest version', done => { + expect(wrapper.vm.diffFilesLength).toEqual(0); + wrapper.vm.glFeatures.diffsBatchLoad = true; + wrapper.vm.isLatestVersion = () => false; + wrapper.vm.fetchData(false); + + expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); + setImmediate(() => { + expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled(); + expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled(); + expect(wrapper.vm.diffFilesLength).toEqual(100); + expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled(); + done(); + }); + }); + + it('calls batch methods if diffsBatchLoad is enabled, and latest version', done => { + expect(wrapper.vm.diffFilesLength).toEqual(0); + wrapper.vm.glFeatures.diffsBatchLoad = true; + wrapper.vm.fetchData(false); + + expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); + setImmediate(() => { + expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled(); + expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled(); + expect(wrapper.vm.diffFilesLength).toEqual(100); + expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('adds container-limiting classes when showFileTree is false with inline diffs', () => { + createComponent({}, ({ state }) => { + state.diffs.showTreeList = false; + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(true); + }); + + it('does not add container-limiting classes when showFileTree is false with inline diffs', () => { + createComponent({}, ({ state }) => { + state.diffs.showTreeList = true; + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false); + }); + + it('does not add container-limiting classes when isFluidLayout', () => { + createComponent({ isFluidLayout: true }, ({ state }) => { + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false); + }); + + it('displays loading icon on loading', () => { + createComponent({}, ({ state }) => { + state.diffs.isLoading = true; + }); + + expect(wrapper.contains(GlLoadingIcon)).toBe(true); + }); + + it('displays loading icon on batch loading', () => { + createComponent({}, ({ state }) => { + state.diffs.isBatchLoading = true; + }); + + expect(wrapper.contains(GlLoadingIcon)).toBe(true); + }); + + it('displays diffs container when not loading', () => { + createComponent(); + + expect(wrapper.contains(GlLoadingIcon)).toBe(false); + expect(wrapper.contains('#diffs')).toBe(true); + }); + + it('does not show commit info', () => { + createComponent(); + + expect(wrapper.contains('.blob-commit-info')).toBe(false); + }); + + describe('row highlighting', () => { + beforeEach(() => { + window.location.hash = 'ABC_123'; + }); + + it('sets highlighted row if hash exists in location object', done => { + createComponent({ + shouldShow: true, + }); + + // Component uses $nextTick so we wait until that has finished + setImmediate(() => { + expect(store.state.diffs.highlightedRow).toBe('ABC_123'); + + done(); + }); + }); + + it('marks current diff file based on currently highlighted row', () => { + createComponent({ + shouldShow: true, + }); + + // Component uses $nextTick so we wait until that has finished + return wrapper.vm.$nextTick().then(() => { + expect(store.state.diffs.currentDiffFileId).toBe('ABC'); + }); + }); + }); + + describe('resizable', () => { + afterEach(() => { + localStorage.removeItem('mr_tree_list_width'); + }); + + it('sets initial width when no localStorage has been set', () => { + createComponent(); + + expect(wrapper.vm.treeWidth).toEqual(320); + }); + + it('sets initial width to localStorage size', () => { + localStorage.setItem('mr_tree_list_width', '200'); + + createComponent(); + + expect(wrapper.vm.treeWidth).toEqual(200); + }); + + it('sets width of tree list', () => { + createComponent(); + + expect(wrapper.find('.js-diff-tree-list').element.style.width).toEqual('320px'); + }); + }); + + it('marks current diff file based on currently highlighted row', done => { + createComponent({ + shouldShow: true, + }); + + // Component uses $nextTick so we wait until that has finished + setImmediate(() => { + expect(store.state.diffs.currentDiffFileId).toBe('ABC'); + + done(); + }); + }); + + describe('empty state', () => { + it('renders empty state when no diff files exist', () => { + createComponent(); + + expect(wrapper.contains(NoChanges)).toBe(true); + }); + + it('does not render empty state when diff files exist', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ + id: 1, + }); + }); + + expect(wrapper.contains(NoChanges)).toBe(false); + expect(wrapper.findAll(DiffFile).length).toBe(1); + }); + + it('does not render empty state when versions match', () => { + createComponent({}, ({ state }) => { + state.diffs.startVersion = mergeRequestDiff; + state.diffs.mergeRequestDiff = mergeRequestDiff; + }); + + expect(wrapper.contains(NoChanges)).toBe(false); + }); + }); + + describe('keyboard shortcut navigation', () => { + const mappings = { + '[': -1, + k: -1, + ']': +1, + j: +1, + }; + let spy; + + describe('visible app', () => { + beforeEach(() => { + spy = jest.fn(); + + createComponent({ + shouldShow: true, + }); + wrapper.setMethods({ + jumpToFile: spy, + }); + }); + + it.each(Object.keys(mappings))( + 'calls `jumpToFile()` with correct parameter whenever pre-defined %s is pressed', + key => { + return wrapper.vm.$nextTick().then(() => { + expect(spy).not.toHaveBeenCalled(); + + Mousetrap.trigger(key); + + expect(spy).toHaveBeenCalledWith(mappings[key]); + }); + }, + ); + + it('does not call `jumpToFile()` when unknown key is pressed', done => { + wrapper.vm + .$nextTick() + .then(() => { + Mousetrap.trigger('d'); + + expect(spy).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('hideen app', () => { + beforeEach(() => { + spy = jest.fn(); + + createComponent({ + shouldShow: false, + }); + wrapper.setMethods({ + jumpToFile: spy, + }); + }); + + it('stops calling `jumpToFile()` when application is hidden', done => { + wrapper.vm + .$nextTick() + .then(() => { + Object.keys(mappings).forEach(key => { + Mousetrap.trigger(key); + + expect(spy).not.toHaveBeenCalled(); + }); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('jumpToFile', () => { + let spy; + + beforeEach(() => { + spy = jest.fn(); + + createComponent({}, () => { + store.state.diffs.diffFiles = [ + { file_hash: '111', file_path: '111.js' }, + { file_hash: '222', file_path: '222.js' }, + { file_hash: '333', file_path: '333.js' }, + ]; + }); + + wrapper.setMethods({ + scrollToFile: spy, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('jumps to next and previous files in the list', done => { + wrapper.vm + .$nextTick() + .then(() => { + wrapper.vm.jumpToFile(+1); + + expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['222.js']); + store.state.diffs.currentDiffFileId = '222'; + wrapper.vm.jumpToFile(+1); + + expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['333.js']); + store.state.diffs.currentDiffFileId = '333'; + wrapper.vm.jumpToFile(-1); + + expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['222.js']); + }) + .then(done) + .catch(done.fail); + }); + + it('does not jump to previous file from the first one', done => { + wrapper.vm + .$nextTick() + .then(() => { + store.state.diffs.currentDiffFileId = '333'; + + expect(wrapper.vm.currentDiffIndex).toEqual(2); + + wrapper.vm.jumpToFile(+1); + + expect(wrapper.vm.currentDiffIndex).toEqual(2); + expect(spy).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('does not jump to next file from the last one', done => { + wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.vm.currentDiffIndex).toEqual(0); + + wrapper.vm.jumpToFile(-1); + + expect(wrapper.vm.currentDiffIndex).toEqual(0); + expect(spy).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('diffs', () => { + it('should render compare versions component', () => { + createComponent({}, ({ state }) => { + state.diffs.mergeRequestDiffs = diffsMockData; + state.diffs.targetBranchName = 'target-branch'; + state.diffs.mergeRequestDiff = mergeRequestDiff; + }); + + expect(wrapper.contains(CompareVersions)).toBe(true); + expect(wrapper.find(CompareVersions).props()).toEqual( + expect.objectContaining({ + targetBranch: { + branchName: 'target-branch', + versionIndex: -1, + path: '', + }, + mergeRequestDiffs: diffsMockData, + mergeRequestDiff, + }), + ); + }); + + it('should render hidden files warning if render overflow warning is present', () => { + createComponent({}, ({ state }) => { + state.diffs.renderOverflowWarning = true; + state.diffs.realSize = '5'; + state.diffs.plainDiffPath = 'plain diff path'; + state.diffs.emailPatchPath = 'email patch path'; + state.diffs.size = 1; + }); + + expect(wrapper.contains(HiddenFilesWarning)).toBe(true); + expect(wrapper.find(HiddenFilesWarning).props()).toEqual( + expect.objectContaining({ + total: '5', + plainDiffPath: 'plain diff path', + emailPatchPath: 'email patch path', + visible: 1, + }), + ); + }); + + it('should display commit widget if store has a commit', () => { + createComponent({}, () => { + store.state.diffs.commit = { + author: 'John Doe', + }; + }); + + expect(wrapper.contains(CommitWidget)).toBe(true); + }); + + it('should display diff file if there are diff files', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + }); + + expect(wrapper.contains(DiffFile)).toBe(true); + }); + + it('should render tree list', () => { + createComponent(); + + expect(wrapper.find(TreeList).exists()).toBe(true); + }); + }); + + describe('hideTreeListIfJustOneFile', () => { + let toggleShowTreeList; + + beforeEach(() => { + toggleShowTreeList = jest.fn(); + }); + + afterEach(() => { + localStorage.removeItem('mr_tree_show'); + }); + + it('calls toggleShowTreeList when only 1 file', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).toHaveBeenCalledWith(false); + }); + + it('does not call toggleShowTreeList when more than 1 file', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + state.diffs.diffFiles.push({ sha: '124' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).not.toHaveBeenCalled(); + }); + + it('does not call toggleShowTreeList when localStorage is set', () => { + localStorage.setItem('mr_tree_show', 'true'); + + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/diffs/create_diffs_store.js b/spec/frontend/diffs/create_diffs_store.js new file mode 100644 index 00000000000..aacde99964c --- /dev/null +++ b/spec/frontend/diffs/create_diffs_store.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import diffsModule from '~/diffs/store/modules'; +import notesModule from '~/notes/stores/modules'; + +Vue.use(Vuex); + +export default function createDiffsStore() { + return new Vuex.Store({ + modules: { + diffs: diffsModule(), + notes: notesModule(), + }, + }); +} |