gitlab-org--gitlab-foss/spec/frontend/diffs/components/diff_file_spec.js

423 lines
14 KiB
JavaScript

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';
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';
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,
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(() => {
wrapper.destroy();
wrapper = null;
});
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', 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(wrapper.find(DiffFileHeaderComponent).exists()).toBe(true);
expect(el.querySelector('.js-syntax-highlight')).toBeDefined();
markFileToBeRendered(store);
await wrapper.vm.$nextTick();
expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
});
});
describe('collapsing', () => {
describe(`\`${EVT_EXPAND_ALL_FILES}\` event`, () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'handleToggle').mockImplementation(() => {});
});
it('performs the normal file toggle when the file is collapsed', async () => {
makeFileAutomaticallyCollapsed(store);
await wrapper.vm.$nextTick();
eventHub.$emit(EVT_EXPAND_ALL_FILES);
expect(wrapper.vm.handleToggle).toHaveBeenCalledTimes(1);
});
it('does nothing when the file is not collapsed', async () => {
eventHub.$emit(EVT_EXPAND_ALL_FILES);
await wrapper.vm.$nextTick();
expect(wrapper.vm.handleToggle).not.toHaveBeenCalled();
});
});
describe('user collapsed', () => {
beforeEach(() => {
makeFileManuallyCollapsed(store);
});
it('should not have any content at all', async () => {
await wrapper.vm.$nextTick();
Array.from(findDiffContentArea(wrapper).element.children).forEach(child => {
expect(isDisplayNone(child)).toBe(true);
});
});
it('should not have the class `has-body` to present the header differently', () => {
expect(wrapper.classes('has-body')).toBe(false);
});
});
describe('automatically collapsed', () => {
beforeEach(() => {
makeFileAutomaticallyCollapsed(store);
});
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);
});
it('should style the component so that it `.has-body` for layout purposes', () => {
expect(wrapper.classes('has-body')).toBe(true);
});
});
describe('not collapsed', () => {
beforeEach(() => {
makeFileOpenByDefault(store);
markFileToBeRendered(store);
});
it('should have the file content', async () => {
expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
});
it('should style the component so that it `.has-body` for layout purposes', () => {
expect(wrapper.classes('has-body')).toBe(true);
});
});
describe('toggle', () => {
it('should update store state', async () => {
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(() => {});
toggleFile(wrapper);
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/setFileCollapsedByUser', {
filePath: wrapper.vm.file.file_path,
collapsed: true,
});
});
describe('fetch collapsed diff', () => {
const prepFile = async (inlineLines, parallelLines, readableText) => {
forceHasDiff({
store,
inlineLines,
parallelLines,
readableText,
});
await wrapper.vm.$nextTick();
toggleFile(wrapper);
};
beforeEach(() => {
jest.spyOn(wrapper.vm, 'requestDiff').mockImplementation(() => {});
makeFileAutomaticallyCollapsed(store);
});
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();
},
);
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);
expect(wrapper.vm.requestDiff).toHaveBeenCalled();
},
);
});
});
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);
});
});
describe('general (other) collapsed', () => {
it('should be expandable for unreadable files', async () => {
({ wrapper, store } = createComponent({ file: getUnreadableFile() }));
makeFileAutomaticallyCollapsed(store);
await wrapper.vm.$nextTick();
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', async () => {
const file = store.state.diffs.diffFiles[0];
const BLOB_LINK = '/file/view/path';
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',
},
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.$el.innerText).toContain(
'This source diff could not be displayed because it is too large',
);
});
});
});