diff options
Diffstat (limited to 'spec/frontend/diffs/components/diff_file_spec.js')
-rw-r--r-- | spec/frontend/diffs/components/diff_file_spec.js | 530 |
1 files changed, 345 insertions, 185 deletions
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js index a6f0d2bf11d..71e0ffd176f 100644 --- a/spec/frontend/diffs/components/diff_file_spec.js +++ b/spec/frontend/diffs/components/diff_file_spec.js @@ -1,262 +1,422 @@ -import Vue from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import { mockTracking, triggerEvent } from 'helpers/tracking_helper'; -import { createStore } from '~/mr_notes/stores'; -import DiffFileComponent from '~/diffs/components/diff_file.vue'; -import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; + +import createDiffsStore from '~/diffs/store/modules'; +import createNotesStore from '~/notes/stores/modules'; import diffFileMockDataReadable from '../mock_data/diff_file'; import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable'; -describe('DiffFile', () => { - let vm; - let trackingSpy; +import DiffFileComponent from '~/diffs/components/diff_file.vue'; +import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue'; +import DiffContentComponent from '~/diffs/components/diff_content.vue'; - beforeEach(() => { - vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { - file: JSON.parse(JSON.stringify(diffFileMockDataReadable)), +import eventHub from '~/diffs/event_hub'; +import { + EVT_EXPAND_ALL_FILES, + EVT_PERF_MARK_DIFF_FILES_END, + EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN, +} from '~/diffs/constants'; + +import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; + +function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) { + const file = store.state.diffs.diffFiles[index]; + const newViewer = { + ...file.viewer, + }; + + if (automaticallyCollapsed !== undefined) { + newViewer.automaticallyCollapsed = automaticallyCollapsed; + } + + if (manuallyCollapsed !== undefined) { + newViewer.manuallyCollapsed = manuallyCollapsed; + } + + if (name !== undefined) { + newViewer.name = name; + } + + Object.assign(file, { + viewer: newViewer, + }); +} + +function forceHasDiff({ store, index = 0, inlineLines, parallelLines, readableText }) { + const file = store.state.diffs.diffFiles[index]; + + Object.assign(file, { + highlighted_diff_lines: inlineLines, + parallel_diff_lines: parallelLines, + blob: { + ...file.blob, + readable_text: readableText, + }, + }); +} + +function markFileToBeRendered(store, index = 0) { + const file = store.state.diffs.diffFiles[index]; + + Object.assign(file, { + renderIt: true, + }); +} + +function createComponent({ file, first = false, last = false }) { + const localVue = createLocalVue(); + + localVue.use(Vuex); + + const store = new Vuex.Store({ + ...createNotesStore(), + modules: { + diffs: createDiffsStore(), + }, + }); + + store.state.diffs.diffFiles = [file]; + + const wrapper = shallowMount(DiffFileComponent, { + store, + localVue, + propsData: { + file, canCurrentUserFork: false, viewDiffsFileByFile: false, - }).$mount(); - trackingSpy = mockTracking('_category_', vm.$el, jest.spyOn); + isFirstFile: first, + isLastFile: last, + }, + }); + + return { + localVue, + wrapper, + store, + }; +} + +const findDiffHeader = wrapper => wrapper.find(DiffFileHeaderComponent); +const findDiffContentArea = wrapper => wrapper.find('[data-testid="content-area"]'); +const findLoader = wrapper => wrapper.find('[data-testid="loader-icon"]'); +const findToggleButton = wrapper => wrapper.find('[data-testid="expand-button"]'); + +const toggleFile = wrapper => findDiffHeader(wrapper).vm.$emit('toggleFile'); +const isDisplayNone = element => element.style.display === 'none'; +const getReadableFile = () => JSON.parse(JSON.stringify(diffFileMockDataReadable)); +const getUnreadableFile = () => JSON.parse(JSON.stringify(diffFileMockDataUnreadable)); + +const makeFileAutomaticallyCollapsed = (store, index = 0) => + changeViewer(store, index, { automaticallyCollapsed: true, manuallyCollapsed: null }); +const makeFileOpenByDefault = (store, index = 0) => + changeViewer(store, index, { automaticallyCollapsed: false, manuallyCollapsed: null }); +const makeFileManuallyCollapsed = (store, index = 0) => + changeViewer(store, index, { automaticallyCollapsed: false, manuallyCollapsed: true }); +const changeViewerType = (store, newType, index = 0) => + changeViewer(store, index, { name: diffViewerModes[newType] }); + +describe('DiffFile', () => { + let wrapper; + let store; + + beforeEach(() => { + ({ wrapper, store } = createComponent({ file: getReadableFile() })); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; }); - const findDiffContent = () => vm.$el.querySelector('.diff-content'); - const isVisible = el => el.style.display !== 'none'; + describe('bus events', () => { + beforeEach(() => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + }); + + describe('during mount', () => { + it.each` + first | last | events | file + ${false} | ${false} | ${[]} | ${{ inlineLines: [], parallelLines: [], readableText: true }} + ${true} | ${true} | ${[]} | ${{ inlineLines: [], parallelLines: [], readableText: true }} + ${true} | ${false} | ${[EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN]} | ${false} + ${false} | ${true} | ${[EVT_PERF_MARK_DIFF_FILES_END]} | ${false} + ${true} | ${true} | ${[EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN, EVT_PERF_MARK_DIFF_FILES_END]} | ${false} + `( + 'emits the events $events based on the file and its position ({ first: $first, last: $last }) among all files', + async ({ file, first, last, events }) => { + if (file) { + forceHasDiff({ store, ...file }); + } + + ({ wrapper, store } = createComponent({ + file: store.state.diffs.diffFiles[0], + first, + last, + })); + + await wrapper.vm.$nextTick(); + + expect(eventHub.$emit).toHaveBeenCalledTimes(events.length); + events.forEach(event => { + expect(eventHub.$emit).toHaveBeenCalledWith(event); + }); + }, + ); + }); + + describe('after loading the diff', () => { + it('indicates that it loaded the file', async () => { + forceHasDiff({ store, inlineLines: [], parallelLines: [], readableText: true }); + ({ wrapper, store } = createComponent({ + file: store.state.diffs.diffFiles[0], + first: true, + last: true, + })); + + jest.spyOn(wrapper.vm, 'loadCollapsedDiff').mockResolvedValue(getReadableFile()); + jest.spyOn(window, 'requestIdleCallback').mockImplementation(fn => fn()); + + makeFileAutomaticallyCollapsed(store); + + await wrapper.vm.$nextTick(); // Wait for store updates to flow into the component + + toggleFile(wrapper); + + await wrapper.vm.$nextTick(); // Wait for the load to resolve + await wrapper.vm.$nextTick(); // Wait for the idleCallback + await wrapper.vm.$nextTick(); // Wait for nextTick inside postRender + + expect(eventHub.$emit).toHaveBeenCalledTimes(2); + expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN); + expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_DIFF_FILES_END); + }); + }); + }); describe('template', () => { - it('should render component with file header, file content components', done => { - const el = vm.$el; - const { file_hash, file_path } = vm.file; + it('should render component with file header, file content components', async () => { + const el = wrapper.vm.$el; + const { file_hash } = wrapper.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('[data-testid="diff-file-copy-clipboard"]')).toBeDefined(); - expect(el.querySelector('.file-title-name').innerText.indexOf(file_path)).toBeGreaterThan(-1); + expect(wrapper.find(DiffFileHeaderComponent).exists()).toBe(true); expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); - vm.file.renderIt = true; + markFileToBeRendered(store); + + await wrapper.vm.$nextTick(); - vm.$nextTick() - .then(() => { - expect(el.querySelectorAll('.line_content').length).toBe(8); - expect(el.querySelectorAll('.js-line-expansion-content').length).toBe(1); - triggerEvent('[data-testid="diff-file-copy-clipboard"]'); - }) - .then(done) - .catch(done.fail); + expect(wrapper.find(DiffContentComponent).exists()).toBe(true); }); + }); - it('should track a click event on copy to clip board button', done => { - const el = vm.$el; + describe('collapsing', () => { + describe(`\`${EVT_EXPAND_ALL_FILES}\` event`, () => { + beforeEach(() => { + jest.spyOn(wrapper.vm, 'handleToggle').mockImplementation(() => {}); + }); - expect(el.querySelector('[data-testid="diff-file-copy-clipboard"]')).toBeDefined(); - vm.file.renderIt = true; - vm.$nextTick() - .then(() => { - triggerEvent('[data-testid="diff-file-copy-clipboard"]'); + it('performs the normal file toggle when the file is collapsed', async () => { + makeFileAutomaticallyCollapsed(store); - expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_copy_file_button', { - label: 'diff_copy_file_path_button', - property: 'diff_copy_file', - }); - }) - .then(done) - .catch(done.fail); - }); + await wrapper.vm.$nextTick(); - 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; + eventHub.$emit(EVT_EXPAND_ALL_FILES); - vm.$nextTick(() => { - expect(isVisible(findDiffContent())).toBe(false); - - done(); - }); + expect(wrapper.vm.handleToggle).toHaveBeenCalledTimes(1); }); - it('should have collapsed text and link', done => { - vm.renderIt = true; - vm.isCollapsed = true; + it('does nothing when the file is not collapsed', async () => { + eventHub.$emit(EVT_EXPAND_ALL_FILES); - vm.$nextTick(() => { - expect(vm.$el.innerText).toContain('This diff is collapsed'); - expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1); + await wrapper.vm.$nextTick(); - done(); - }); + expect(wrapper.vm.handleToggle).not.toHaveBeenCalled(); }); + }); - it('should have collapsed text and link even before rendered', done => { - vm.renderIt = false; - vm.isCollapsed = true; + describe('user collapsed', () => { + beforeEach(() => { + makeFileManuallyCollapsed(store); + }); - vm.$nextTick(() => { - expect(vm.$el.innerText).toContain('This diff is collapsed'); - expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1); + it('should not have any content at all', async () => { + await wrapper.vm.$nextTick(); - done(); + Array.from(findDiffContentArea(wrapper).element.children).forEach(child => { + expect(isDisplayNone(child)).toBe(true); }); }); - it('should be collapsable for unreadable files', done => { - vm.$destroy(); - vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { - file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)), - canCurrentUserFork: false, - viewDiffsFileByFile: false, - }).$mount(); + it('should not have the class `has-body` to present the header differently', () => { + expect(wrapper.classes('has-body')).toBe(false); + }); + }); - vm.renderIt = false; - vm.isCollapsed = true; + describe('automatically collapsed', () => { + beforeEach(() => { + makeFileAutomaticallyCollapsed(store); + }); - vm.$nextTick(() => { - expect(vm.$el.innerText).toContain('This diff is collapsed'); - expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1); + it('should show the collapsed file warning with expansion button', () => { + expect(findDiffContentArea(wrapper).html()).toContain( + 'Files with large changes are collapsed by default.', + ); + expect(findToggleButton(wrapper).exists()).toBe(true); + }); - done(); - }); + it('should style the component so that it `.has-body` for layout purposes', () => { + expect(wrapper.classes('has-body')).toBe(true); }); + }); - 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; + describe('not collapsed', () => { + beforeEach(() => { + makeFileOpenByDefault(store); + markFileToBeRendered(store); + }); - vm.$nextTick(() => { - expect(vm.$el.innerText).not.toContain('This diff is collapsed'); + it('should have the file content', async () => { + expect(wrapper.find(DiffContentComponent).exists()).toBe(true); + }); - done(); - }); + it('should style the component so that it `.has-body` for layout purposes', () => { + expect(wrapper.classes('has-body')).toBe(true); }); + }); - 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; + describe('toggle', () => { + it('should update store state', async () => { + jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(() => {}); - vm.$nextTick(() => { - expect(vm.$el.innerText).not.toContain('This diff is collapsed'); + toggleFile(wrapper); - done(); + expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/setFileCollapsedByUser', { + filePath: wrapper.vm.file.file_path, + collapsed: true, }); }); - it('should have loading icon while loading a collapsed diffs', done => { - vm.isCollapsed = true; - vm.isLoadingCollapsedDiff = true; + describe('fetch collapsed diff', () => { + const prepFile = async (inlineLines, parallelLines, readableText) => { + forceHasDiff({ + store, + inlineLines, + parallelLines, + readableText, + }); + + await wrapper.vm.$nextTick(); - vm.$nextTick(() => { - expect(vm.$el.querySelectorAll('.diff-content.loading').length).toEqual(1); + toggleFile(wrapper); + }; - done(); + beforeEach(() => { + jest.spyOn(wrapper.vm, 'requestDiff').mockImplementation(() => {}); + + makeFileAutomaticallyCollapsed(store); }); - }); - it('should update store state', done => { - jest.spyOn(vm.$store, 'dispatch').mockImplementation(() => {}); + it.each` + inlineLines | parallelLines | readableText + ${[1]} | ${[1]} | ${true} + ${[]} | ${[1]} | ${true} + ${[1]} | ${[]} | ${true} + ${[1]} | ${[1]} | ${false} + ${[]} | ${[]} | ${false} + `( + 'does not make a request to fetch the diff for a diff file like { inline: $inlineLines, parallel: $parallelLines, readableText: $readableText }', + async ({ inlineLines, parallelLines, readableText }) => { + await prepFile(inlineLines, parallelLines, readableText); + + expect(wrapper.vm.requestDiff).not.toHaveBeenCalled(); + }, + ); - vm.isCollapsed = true; + it.each` + inlineLines | parallelLines | readableText + ${[]} | ${[]} | ${true} + `( + 'makes a request to fetch the diff for a diff file like { inline: $inlineLines, parallel: $parallelLines, readableText: $readableText }', + async ({ inlineLines, parallelLines, readableText }) => { + await prepFile(inlineLines, parallelLines, readableText); - vm.$nextTick(() => { - expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/setFileCollapsed', { - filePath: vm.file.file_path, - collapsed: true, - }); + expect(wrapper.vm.requestDiff).toHaveBeenCalled(); + }, + ); + }); + }); - done(); - }); + describe('loading', () => { + it('should have loading icon while loading a collapsed diffs', async () => { + makeFileAutomaticallyCollapsed(store); + wrapper.vm.isLoadingCollapsedDiff = true; + + await wrapper.vm.$nextTick(); + + expect(findLoader(wrapper).exists()).toBe(true); }); + }); - it('updates local state when changing file state', done => { - vm.file.viewer.automaticallyCollapsed = true; + describe('general (other) collapsed', () => { + it('should be expandable for unreadable files', async () => { + ({ wrapper, store } = createComponent({ file: getUnreadableFile() })); + makeFileAutomaticallyCollapsed(store); - vm.$nextTick(() => { - expect(vm.isCollapsed).toBe(true); + await wrapper.vm.$nextTick(); - done(); - }); + expect(findDiffContentArea(wrapper).html()).toContain( + 'Files with large changes are collapsed by default.', + ); + expect(findToggleButton(wrapper).exists()).toBe(true); }); + + it.each` + mode + ${'renamed'} + ${'mode_changed'} + `( + 'should render the DiffContent component for files whose mode is $mode', + async ({ mode }) => { + makeFileOpenByDefault(store); + markFileToBeRendered(store); + changeViewerType(store, mode); + + await wrapper.vm.$nextTick(); + + expect(wrapper.classes('has-body')).toBe(true); + expect(wrapper.find(DiffContentComponent).exists()).toBe(true); + expect(wrapper.find(DiffContentComponent).isVisible()).toBe(true); + }, + ); }); }); describe('too large diff', () => { - it('should have too large warning and blob link', done => { + it('should have too large warning and blob link', async () => { + const file = store.state.diffs.diffFiles[0]; 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(); + Object.assign(store.state.diffs.diffFiles[0], { + ...file, + view_path: BLOB_LINK, + renderIt: true, + viewer: { + ...file.viewer, + error: diffViewerErrors.too_large, + error_message: 'This source diff could not be displayed because it is too large', + }, }); - }); - }); - describe('watch collapsed', () => { - it('calls handleLoadCollapsedDiff if collapsed changed & file has no lines', done => { - jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {}); - - vm.file.highlighted_diff_lines = []; - 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); - }); + await wrapper.vm.$nextTick(); - 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, - viewDiffsFileByFile: false, - }).$mount(); - - jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {}); - - vm.file.highlighted_diff_lines = []; - vm.file.parallel_diff_lines = undefined; - vm.isCollapsed = true; - - vm.$nextTick() - .then(() => { - vm.isCollapsed = false; - - return vm.$nextTick(); - }) - .then(() => { - expect(vm.handleLoadCollapsedDiff).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + expect(wrapper.vm.$el.innerText).toContain( + 'This source diff could not be displayed because it is too large', + ); }); }); }); |