Commit 441ea69a authored by Phil Hughes's avatar Phil Hughes

Merge branch 'feature/collapsed-diff-files/user-collapsed-flag' into 'master'

Add `manuallyCollapsed` flag to Diff Files

See merge request gitlab-org/gitlab!43911
parents 41543e44 d6c42a8b
......@@ -124,7 +124,6 @@ export default {
return {
treeWidth,
diffFilesLength: 0,
collapsedWarningDismissed: false,
};
},
computed: {
......@@ -153,7 +152,7 @@ export default {
'canMerge',
'hasConflicts',
]),
...mapGetters('diffs', ['hasCollapsedFile', 'isParallelView', 'currentDiffIndex']),
...mapGetters('diffs', ['whichCollapsedTypes', 'isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
if (!this.viewDiffsFileByFile) {
......@@ -206,11 +205,7 @@ export default {
visible = this.$options.alerts.ALERT_OVERFLOW_HIDDEN;
} else if (this.isDiffHead && this.hasConflicts) {
visible = this.$options.alerts.ALERT_MERGE_CONFLICT;
} else if (
this.hasCollapsedFile &&
!this.collapsedWarningDismissed &&
!this.viewDiffsFileByFile
) {
} else if (this.whichCollapsedTypes.automatic && !this.viewDiffsFileByFile) {
visible = this.$options.alerts.ALERT_COLLAPSED_FILES;
}
......@@ -429,9 +424,6 @@ export default {
this.toggleShowTreeList(false);
}
},
dismissCollapsedWarning() {
this.collapsedWarningDismissed = true;
},
},
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
......@@ -464,7 +456,6 @@ export default {
<collapsed-files-warning
v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES"
:limited="isLimitedContainer"
@dismiss="dismissCollapsedWarning"
/>
<div
......
......@@ -38,7 +38,7 @@ export default {
},
computed: {
...mapGetters('diffs', [
'hasCollapsedFile',
'whichCollapsedTypes',
'diffCompareDropdownTargetVersions',
'diffCompareDropdownSourceVersions',
]),
......@@ -129,7 +129,7 @@ export default {
{{ __('Show latest version') }}
</gl-button>
<gl-button
v-show="hasCollapsedFile"
v-show="whichCollapsedTypes.any"
variant="default"
class="gl-mr-3"
@click="expandAllFiles"
......
......@@ -10,6 +10,8 @@ import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants';
import { collapsedType, isCollapsed } from '../diff_file';
import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE } from '../constants';
export default {
components: {
......@@ -44,7 +46,7 @@ export default {
return {
isLoadingCollapsedDiff: false,
forkMessageVisible: false,
isCollapsed: this.file.viewer.automaticallyCollapsed || false,
isCollapsed: isCollapsed(this.file),
};
},
computed: {
......@@ -71,7 +73,7 @@ export default {
return this.file.viewer.error === diffViewerErrors.too_large;
},
errorMessage() {
return this.file.viewer.error_message;
return !this.manuallyCollapsed ? this.file.viewer.error_message : '';
},
forkMessage() {
return sprintf(
......@@ -85,57 +87,94 @@ export default {
false,
);
},
},
watch: {
isCollapsed: function fileCollapsedWatch(newVal, oldVal) {
if (!newVal && oldVal && !this.hasDiff) {
this.handleLoadCollapsedDiff();
hasBodyClasses() {
const domParts = {
header: 'gl-rounded-base!',
contentByHash: '',
content: '',
};
if (this.showBody) {
domParts.header = 'gl-rounded-bottom-left-none gl-rounded-bottom-right-none';
domParts.contentByHash =
'gl-rounded-none gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-border-1 gl-border-t-0! gl-border-solid gl-border-gray-100';
domParts.content = 'gl-rounded-bottom-left-base gl-rounded-bottom-right-base';
}
this.setFileCollapsed({ filePath: this.file.file_path, collapsed: newVal });
return domParts;
},
automaticallyCollapsed() {
return collapsedType(this.file) === DIFF_FILE_AUTOMATIC_COLLAPSE;
},
manuallyCollapsed() {
return collapsedType(this.file) === DIFF_FILE_MANUAL_COLLAPSE;
},
showBody() {
return !this.isCollapsed || this.automaticallyCollapsed;
},
showWarning() {
return this.isCollapsed && (this.automaticallyCollapsed && !this.viewDiffsFileByFile);
},
showContent() {
return !this.isCollapsed && !this.isFileTooLarge;
},
},
watch: {
'file.file_hash': {
handler: function watchFileHash() {
if (this.viewDiffsFileByFile && this.file.viewer.automaticallyCollapsed) {
this.isCollapsed = false;
this.handleLoadCollapsedDiff();
} else {
this.isCollapsed = this.file.viewer.automaticallyCollapsed || false;
handler: function hashChangeWatch(newHash, oldHash) {
this.isCollapsed = isCollapsed(this.file);
if (newHash && oldHash && !this.hasDiff) {
this.requestDiff();
}
},
immediate: true,
},
'file.viewer.automaticallyCollapsed': function setIsCollapsed(newVal) {
if (!this.viewDiffsFileByFile) {
this.isCollapsed = newVal;
'file.viewer.automaticallyCollapsed': {
handler: function autoChangeWatch(automaticValue) {
if (collapsedType(this.file) !== DIFF_FILE_MANUAL_COLLAPSE) {
this.isCollapsed = this.viewDiffsFileByFile ? false : automaticValue;
}
},
immediate: true,
},
'file.viewer.manuallyCollapsed': {
handler: function manualChangeWatch(manualValue) {
if (manualValue !== null) {
this.isCollapsed = manualValue;
}
},
immediate: true,
},
},
created() {
eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.handleLoadCollapsedDiff);
eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff);
},
methods: {
...mapActions('diffs', [
'loadCollapsedDiff',
'assignDiscussionsToDiff',
'setRenderIt',
'setFileCollapsed',
'setFileCollapsedByUser',
]),
handleToggle() {
if (!this.hasDiff) {
this.handleLoadCollapsedDiff();
} else {
this.isCollapsed = !this.isCollapsed;
this.setRenderIt(this.file);
const currentCollapsedFlag = this.isCollapsed;
this.setFileCollapsedByUser({
filePath: this.file.file_path,
collapsed: !currentCollapsedFlag,
});
if (!this.hasDiff && currentCollapsedFlag) {
this.requestDiff();
}
},
handleLoadCollapsedDiff() {
requestDiff() {
this.isLoadingCollapsedDiff = true;
this.loadCollapsedDiff(this.file)
.then(() => {
this.isLoadingCollapsedDiff = false;
this.isCollapsed = false;
this.setRenderIt(this.file);
})
.then(() => {
......@@ -167,9 +206,10 @@ export default {
:class="{
'is-active': currentDiffFileId === file.file_hash,
'comments-disabled': Boolean(file.brokenSymlink),
'has-body': showBody,
}"
:data-path="file.new_path"
class="diff-file file-holder"
class="diff-file file-holder gl-border-none"
>
<diff-file-header
:can-current-user-fork="canCurrentUserFork"
......@@ -178,7 +218,8 @@ export default {
:expanded="!isCollapsed"
:add-merge-request-buttons="true"
:view-diffs-file-by-file="viewDiffsFileByFile"
class="js-file-title file-title"
class="js-file-title file-title gl-border-1 gl-border-solid gl-border-gray-100"
:class="hasBodyClasses.header"
@toggleFile="handleToggle"
@showForkMessage="showForkMessage"
/>
......@@ -198,21 +239,35 @@ export default {
{{ __('Cancel') }}
</button>
</div>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
<template v-else>
<div :id="`diff-content-${file.file_hash}`">
<div v-if="errorMessage" class="diff-viewer">
<div
:id="`diff-content-${file.file_hash}`"
:class="hasBodyClasses.contentByHash"
data-testid="content-area"
>
<gl-loading-icon
v-if="showLoadingIcon"
class="diff-content loading gl-my-0 gl-pt-3"
data-testid="loader-icon"
/>
<div v-else-if="errorMessage" class="diff-viewer">
<div v-safe-html="errorMessage" class="nothing-here-block"></div>
</div>
<template v-else>
<div v-show="isCollapsed" class="nothing-here-block diff-collapsed">
<div v-show="showWarning" class="nothing-here-block diff-collapsed">
{{ __('This diff is collapsed.') }}
<a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
__('Click to expand it.')
}}</a>
<a
class="click-to-expand"
data-testid="toggle-link"
href="#"
@click.prevent="handleToggle"
>
{{ __('Click to expand it.') }}
</a>
</div>
<diff-content
v-show="!isCollapsed && !isFileTooLarge"
v-show="showContent"
:class="hasBodyClasses.content"
:diff-file="file"
:help-page-path="helpPagePath"
/>
......
......@@ -18,6 +18,7 @@ import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants';
import DiffStats from './diff_stats.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
import { isCollapsed } from '../diff_file';
import { DIFF_FILE_HEADER } from '../i18n';
export default {
......@@ -125,6 +126,9 @@ export default {
isUsingLfs() {
return this.diffFile.stored_externally && this.diffFile.external_storage === 'lfs';
},
isCollapsed() {
return isCollapsed(this.diffFile, { fileByFile: this.viewDiffsFileByFile });
},
collapseIcon() {
return this.expanded ? 'chevron-down' : 'chevron-right';
},
......@@ -334,7 +338,7 @@ export default {
</gl-dropdown-item>
</template>
<template v-if="!diffFile.viewer.automaticallyCollapsed">
<template v-if="!isCollapsed">
<gl-dropdown-divider
v-if="!diffFile.is_fully_expanded || diffHasDiscussions(diffFile)"
/>
......
......@@ -73,6 +73,10 @@ export const ALERT_OVERFLOW_HIDDEN = 'overflow';
export const ALERT_MERGE_CONFLICT = 'merge-conflict';
export const ALERT_COLLAPSED_FILES = 'collapsed';
// Diff File collapse types
export const DIFF_FILE_AUTOMATIC_COLLAPSE = 'automatic';
export const DIFF_FILE_MANUAL_COLLAPSE = 'manual';
// State machine states
export const STATE_IDLING = 'idle';
export const STATE_LOADING = 'loading';
......
import { DIFF_FILE_SYMLINK_MODE, DIFF_FILE_DELETED_MODE } from './constants';
import {
DIFF_FILE_SYMLINK_MODE,
DIFF_FILE_DELETED_MODE,
DIFF_FILE_MANUAL_COLLAPSE,
DIFF_FILE_AUTOMATIC_COLLAPSE,
} from './constants';
function fileSymlinkInformation(file, fileList) {
const duplicates = fileList.filter(iteratedFile => iteratedFile.file_hash === file.file_hash);
......@@ -23,6 +28,7 @@ function collapsed(file) {
return {
automaticallyCollapsed: viewer.automaticallyCollapsed || viewer.collapsed || false,
manuallyCollapsed: null,
};
}
......@@ -37,3 +43,19 @@ export function prepareRawDiffFile({ file, allFiles }) {
return file;
}
export function collapsedType(file) {
const isManual = typeof file.viewer?.manuallyCollapsed === 'boolean';
return isManual ? DIFF_FILE_MANUAL_COLLAPSE : DIFF_FILE_AUTOMATIC_COLLAPSE;
}
export function isCollapsed(file) {
const type = collapsedType(file);
const collapsedStates = {
[DIFF_FILE_AUTOMATIC_COLLAPSE]: file.viewer?.automaticallyCollapsed || false,
[DIFF_FILE_MANUAL_COLLAPSE]: file.viewer?.manuallyCollapsed,
};
return collapsedStates[type];
}
......@@ -40,8 +40,11 @@ import {
DIFF_WHITESPACE_COOKIE_NAME,
SHOW_WHITESPACE,
NO_SHOW_WHITESPACE,
DIFF_FILE_MANUAL_COLLAPSE,
DIFF_FILE_AUTOMATIC_COLLAPSE,
} from '../constants';
import { diffViewerModes } from '~/ide/constants';
import { isCollapsed } from '../diff_file';
export const setBaseConfig = ({ commit }, options) => {
const {
......@@ -239,6 +242,13 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi
if (file.viewer.automaticallyCollapsed) {
eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`);
scrollToElement(document.getElementById(file.file_hash));
} else if (file.viewer.manuallyCollapsed) {
commit(types.SET_FILE_COLLAPSED, {
filePath: file.file_path,
collapsed: false,
trigger: DIFF_FILE_AUTOMATIC_COLLAPSE,
});
eventHub.$emit('scrollToDiscussion');
} else {
eventHub.$emit('scrollToDiscussion');
}
......@@ -252,8 +262,7 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
const nextFile = state.diffFiles.find(
file =>
!file.renderIt &&
(file.viewer &&
(!file.viewer.automaticallyCollapsed || file.viewer.name !== diffViewerModes.text)),
(file.viewer && (!isCollapsed(file) || file.viewer.name !== diffViewerModes.text)),
);
if (nextFile) {
......@@ -641,8 +650,9 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
});
}
export const setFileCollapsed = ({ commit }, { filePath, collapsed }) =>
commit(types.SET_FILE_COLLAPSED, { filePath, collapsed });
export const setFileCollapsedByUser = ({ commit }, { filePath, collapsed }) => {
commit(types.SET_FILE_COLLAPSED, { filePath, collapsed, trigger: DIFF_FILE_MANUAL_COLLAPSE });
};
export const setSuggestPopoverDismissed = ({ commit, state }) =>
axios
......
......@@ -8,8 +8,16 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
export const hasCollapsedFile = state =>
state.diffFiles.some(file => file.viewer && file.viewer.automaticallyCollapsed);
export const whichCollapsedTypes = state => {
const automatic = state.diffFiles.some(file => file.viewer?.automaticallyCollapsed);
const manual = state.diffFiles.some(file => file.viewer?.manuallyCollapsed);
return {
any: automatic || manual,
automatic,
manual,
};
};
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
......
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { INLINE_DIFF_VIEW_TYPE } from '../constants';
import {
DIFF_FILE_MANUAL_COLLAPSE,
DIFF_FILE_AUTOMATIC_COLLAPSE,
INLINE_DIFF_VIEW_TYPE,
} from '../constants';
import {
findDiffFile,
addLineReferences,
......@@ -16,6 +20,12 @@ function updateDiffFilesInState(state, files) {
return Object.assign(state, { diffFiles: files });
}
function renderFile(file) {
Object.assign(file, {
renderIt: true,
});
}
export default {
[types.SET_BASE_CONFIG](state, options) {
const {
......@@ -81,9 +91,7 @@ export default {
},
[types.RENDER_FILE](state, file) {
Object.assign(file, {
renderIt: true,
});
renderFile(file);
},
[types.SET_MERGE_REQUEST_DIFFS](state, mergeRequestDiffs) {
......@@ -173,6 +181,7 @@ export default {
Object.assign(file, {
viewer: Object.assign(file.viewer, {
automaticallyCollapsed: false,
manuallyCollapsed: false,
}),
});
});
......@@ -351,11 +360,24 @@ export default {
file.isShowingFullFile = true;
file.isLoadingFullFile = false;
},
[types.SET_FILE_COLLAPSED](state, { filePath, collapsed }) {
[types.SET_FILE_COLLAPSED](
state,
{ filePath, collapsed, trigger = DIFF_FILE_AUTOMATIC_COLLAPSE },
) {
const file = state.diffFiles.find(f => f.file_path === filePath);
if (file && file.viewer) {
if (trigger === DIFF_FILE_MANUAL_COLLAPSE) {
file.viewer.automaticallyCollapsed = false;
file.viewer.manuallyCollapsed = collapsed;
} else if (trigger === DIFF_FILE_AUTOMATIC_COLLAPSE) {
file.viewer.automaticallyCollapsed = collapsed;
file.viewer.manuallyCollapsed = null;
}
}
if (file && !collapsed) {
renderFile(file);
}
},
[types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
......
......@@ -7,6 +7,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { getDiffMode } from '~/diffs/store/utils';
import { diffViewerModes } from '~/ide/constants';
import { isCollapsed } from '../../diffs/diff_file';
const FIRST_CHAR_REGEX = /^(\+|-| )/;
......@@ -46,6 +47,9 @@ export default {
this.discussion.truncated_diff_lines && this.discussion.truncated_diff_lines.length !== 0
);
},
isCollapsed() {
return isCollapsed(this.discussion.diff_file);
},
},
mounted() {
if (this.isTextFile && !this.hasTruncatedDiffLines) {
......@@ -76,7 +80,7 @@ export default {
:discussion-path="discussion.discussion_path"
:diff-file="discussion.diff_file"
:can-current-user-fork="false"
:expanded="!discussion.diff_file.viewer.automaticallyCollapsed"
:expanded="!isCollapsed"
/>
<div v-if="isTextFile" class="diff-content">
<table class="code js-syntax-highlight" :class="$options.userColorSchemeClass">
......
......@@ -6,11 +6,18 @@
border-top: 1px solid $border-color;
}
&.has-body {
.file-title {
box-shadow: 0 -2px 0 0 var(--white);
}
}
table.code tr:last-of-type td:last-of-type {
@include gl-rounded-bottom-right-base();
}
.file-title,
.file-title-flex-parent {
border-top-left-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
box-shadow: 0 -2px 0 0 var(--white);
cursor: pointer;
.dropdown-menu {
......@@ -113,7 +120,6 @@
.diff-content {
background: $white;
color: $gl-text-color;
border-radius: 0 0 3px 3px;
.unfold {
cursor: pointer;
......@@ -457,9 +463,12 @@ table.code {
border-top: 0;
}
tr:nth-last-of-type(2).line_expansion > td {
tr:nth-last-of-type(2).line_expansion,
tr:last-of-type.line_expansion {
> td {
border-bottom: 0;
}
}
tr.line_holder td {
line-height: $code-line-height;
......
......@@ -11,9 +11,19 @@
}
.diff-tree-list {
// This 11px value should match the additional value found in
// /assets/stylesheets/framework/diffs.scss
// for the $mr-file-header-top SCSS variable within the
// .file-title,
// .file-title-flex-parent {
// rule.
// If they don't match, the file tree and the diff files stick
// to the top at different heights, which is a bad-looking defect
$diff-file-header-top: 11px;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + $diff-file-header-top;
position: -webkit-sticky;
position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
top: $top-pos;
max-height: calc(100vh - #{$top-pos});
z-index: 202;
......
---
title: Manually collapsed diff files are now significantly shorter and less visually
intrusive
merge_request: 43911
author:
type: changed
......@@ -697,7 +697,7 @@ describe('diffs/components/app', () => {
});
describe('collapsed files', () => {
it('should render the collapsed files warning if there are any collapsed files', () => {
it('should render the collapsed files warning if there are any automatically collapsed files', () => {
createComponent({}, ({ state }) => {
state.diffs.diffFiles = [{ viewer: { automaticallyCollapsed: true } }];
});
......@@ -705,16 +705,14 @@ describe('diffs/components/app', () => {
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true);
});
it('should not render the collapsed files warning if the user has dismissed the alert already', async () => {
it('should not render the collapsed files warning if there are no automatically collapsed files', () => {
createComponent({}, ({ state }) => {
state.diffs.diffFiles = [{ viewer: { automaticallyCollapsed: true } }];
state.diffs.diffFiles = [
{ viewer: { automaticallyCollapsed: false, manuallyCollapsed: true } },
{ viewer: { automaticallyCollapsed: false, manuallyCollapsed: false } },
];
});
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true);
wrapper.vm.collapsedWarningDismissed = true;
await wrapper.vm.$nextTick();
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(false);
});
});
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { cloneDeep } from 'lodash';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import diffDiscussionsMockData from '../mock_data/diff_discussions';
......@@ -136,11 +139,27 @@ describe('DiffFileHeader component', () => {
});
});
it('displays a copy to clipboard button', () => {
describe('copy to clipboard', () => {
beforeEach(() => {
createComponent();
});
it('displays a copy to clipboard button', () => {
expect(wrapper.find(ClipboardButton).exists()).toBe(true);
});
it('triggers the copy to clipboard tracking event', () => {
const trackingSpy = mockTracking('_category_', wrapper.vm.$el, jest.spyOn);
triggerEvent('[data-testid="diff-file-copy-clipboard"]');
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_copy_file_button', {
label: 'diff_copy_file_path_button',
property: 'diff_copy_file',
});
});
});
describe('for submodule', () => {
const submoduleDiffFile = {
...diffFile,
......
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 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 { 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 }) {
const localVue = createLocalVue();
localVue.use(Vuex);
const store = new Vuex.Store({
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);
},
});
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 findToggleLinks = wrapper => wrapper.findAll('[data-testid="toggle-link"]');
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();
});
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;
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);
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);
});
it('should track a click event on copy to clip board button', done => {
const el = vm.$el;
expect(el.querySelector('[data-testid="diff-file-copy-clipboard"]')).toBeDefined();
vm.file.renderIt = true;
vm.$nextTick()
.then(() => {
triggerEvent('[data-testid="diff-file-copy-clipboard"]');
await wrapper.vm.$nextTick();
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_copy_file_button', {
label: 'diff_copy_file_path_button',
property: 'diff_copy_file',
expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
});
})
.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;
describe('collapsing', () => {
describe('user collapsed', () => {
beforeEach(() => {
makeFileManuallyCollapsed(store);
});
vm.$nextTick(() => {
expect(isVisible(findDiffContent())).toBe(false);
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 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 not have the class `has-body` to present the header differently', () => {
expect(wrapper.classes('has-body')).toBe(false);
});
});
it('should have collapsed text and link even before rendered', done => {
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 link', () => {
expect(findDiffContentArea(wrapper).html()).toContain('This diff is collapsed');
expect(findToggleLinks(wrapper).length).toEqual(1);
});
done();
it('should style the component so that it `.has-body` for layout purposes', () => {
expect(wrapper.classes('has-body')).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();
vm.renderIt = false;
vm.isCollapsed = true;
describe('not collapsed', () => {
beforeEach(() => {
makeFileOpenByDefault(store);
markFileToBeRendered(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 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 renamed files', done => {
vm.renderIt = true;
vm.isCollapsed = false;
vm.file.highlighted_diff_lines = null;
vm.file.viewer.name = diffViewerModes.renamed;
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 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();
});
describe('fetch collapsed diff', () => {
const prepFile = async (inlineLines, parallelLines, readableText) => {
forceHasDiff({
store,
inlineLines,
parallelLines,
readableText,
});
it('should have loading icon while loading a collapsed diffs', done => {
vm.isCollapsed = true;
vm.isLoadingCollapsedDiff = true;
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);
vm.isCollapsed = true;
expect(wrapper.vm.requestDiff).not.toHaveBeenCalled();
},
);
vm.$nextTick(() => {
expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/setFileCollapsed', {
filePath: vm.file.file_path,
collapsed: 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);
done();
expect(wrapper.vm.requestDiff).toHaveBeenCalled();
},
);
});
});
it('updates local state when changing file state', done => {
vm.file.viewer.automaticallyCollapsed = true;
describe('loading', () => {
it('should have loading icon while loading a collapsed diffs', async () => {
makeFileAutomaticallyCollapsed(store);
wrapper.vm.isLoadingCollapsedDiff = true;
vm.$nextTick(() => {
expect(vm.isCollapsed).toBe(true);
await wrapper.vm.$nextTick();
done();
});
});
expect(findLoader(wrapper).exists()).toBe(true);
});
});
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',
);
describe('general (other) collapsed', () => {
it('should be expandable for unreadable files', async () => {
({ wrapper, store } = createComponent({ file: getUnreadableFile() }));
makeFileAutomaticallyCollapsed(store);
done();
});
});
});
await wrapper.vm.$nextTick();
describe('watch collapsed', () => {
it('calls handleLoadCollapsedDiff if collapsed changed & file has no lines', done => {
jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {});
expect(findDiffContentArea(wrapper).html()).toContain('This diff is collapsed');
expect(findToggleLinks(wrapper).length).toEqual(1);
});
vm.file.highlighted_diff_lines = [];
vm.file.parallel_diff_lines = [];
vm.isCollapsed = 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);
vm.$nextTick()
.then(() => {
vm.isCollapsed = false;
await wrapper.vm.$nextTick();
return vm.$nextTick();
})
.then(() => {
expect(vm.handleLoadCollapsedDiff).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
expect(wrapper.classes('has-body')).toBe(true);
expect(wrapper.find(DiffContentComponent).exists()).toBe(true);
expect(wrapper.find(DiffContentComponent).isVisible()).toBe(true);
},
);
});
});
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(() => {});
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';
vm.file.highlighted_diff_lines = [];
vm.file.parallel_diff_lines = undefined;
vm.isCollapsed = true;
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',
},
});
vm.$nextTick()
.then(() => {
vm.isCollapsed = false;
await wrapper.vm.$nextTick();
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',
);
});
});
});
......@@ -27,6 +27,7 @@ export default {
name: 'text',
error: null,
automaticallyCollapsed: false,
manuallyCollapsed: null,
},
added_lines: 2,
removed_lines: 0,
......
......@@ -26,6 +26,7 @@ export default {
name: 'text',
error: null,
automaticallyCollapsed: false,
manuallyCollapsed: null,
},
added_lines: 0,
removed_lines: 0,
......
......@@ -42,7 +42,7 @@ import {
fetchFullDiff,
toggleFullDiff,
switchToFullDiffFromRenamedFile,
setFileCollapsed,
setFileCollapsedByUser,
setExpandedDiffLines,
setSuggestPopoverDismissed,
changeCurrentCommit,
......@@ -1216,13 +1216,18 @@ describe('DiffsStoreActions', () => {
});
});
describe('setFileCollapsed', () => {
describe('setFileUserCollapsed', () => {
it('commits SET_FILE_COLLAPSED', done => {
testAction(
setFileCollapsed,
setFileCollapsedByUser,
{ filePath: 'test', collapsed: true },
null,
[{ type: types.SET_FILE_COLLAPSED, payload: { filePath: 'test', collapsed: true } }],
[
{
type: types.SET_FILE_COLLAPSED,
payload: { filePath: 'test', collapsed: true, trigger: 'manual' },
},
],
[],
done,
);
......
......@@ -49,23 +49,53 @@ describe('Diffs Module Getters', () => {
});
});
describe('hasCollapsedFile', () => {
it('returns true when all files are collapsed', () => {
localState.diffFiles = [
{ viewer: { automaticallyCollapsed: true } },
{ viewer: { automaticallyCollapsed: true } },
];
describe('whichCollapsedTypes', () => {
const autoCollapsedFile = { viewer: { automaticallyCollapsed: true, manuallyCollapsed: null } };
const manuallyCollapsedFile = {
viewer: { automaticallyCollapsed: false, manuallyCollapsed: true },
};
const openFile = { viewer: { automaticallyCollapsed: false, manuallyCollapsed: false } };
it.each`
description | value | files
${'all files are automatically collapsed'} | ${true} | ${[{ ...autoCollapsedFile }, { ...autoCollapsedFile }]}
${'all files are manually collapsed'} | ${true} | ${[{ ...manuallyCollapsedFile }, { ...manuallyCollapsedFile }]}
${'no files are collapsed in any way'} | ${false} | ${[{ ...openFile }, { ...openFile }]}
${'some files are collapsed in either way'} | ${true} | ${[{ ...manuallyCollapsedFile }, { ...autoCollapsedFile }, { ...openFile }]}
`('`any` is $value when $description', ({ value, files }) => {
localState.diffFiles = files;
const getterResult = getters.whichCollapsedTypes(localState);
expect(getterResult.any).toEqual(value);
});
it.each`
description | value | files
${'all files are automatically collapsed'} | ${true} | ${[{ ...autoCollapsedFile }, { ...autoCollapsedFile }]}
${'all files are manually collapsed'} | ${false} | ${[{ ...manuallyCollapsedFile }, { ...manuallyCollapsedFile }]}
${'no files are collapsed in any way'} | ${false} | ${[{ ...openFile }, { ...openFile }]}
${'some files are collapsed in either way'} | ${true} | ${[{ ...manuallyCollapsedFile }, { ...autoCollapsedFile }, { ...openFile }]}
`('`automatic` is $value when $description', ({ value, files }) => {
localState.diffFiles = files;
expect(getters.hasCollapsedFile(localState)).toEqual(true);
const getterResult = getters.whichCollapsedTypes(localState);
expect(getterResult.automatic).toEqual(value);
});
it('returns true when at least one file is collapsed', () => {
localState.diffFiles = [
{ viewer: { automaticallyCollapsed: false } },
{ viewer: { automaticallyCollapsed: true } },
];
it.each`
description | value | files
${'all files are automatically collapsed'} | ${false} | ${[{ ...autoCollapsedFile }, { ...autoCollapsedFile }]}
${'all files are manually collapsed'} | ${true} | ${[{ ...manuallyCollapsedFile }, { ...manuallyCollapsedFile }]}
${'no files are collapsed in any way'} | ${false} | ${[{ ...openFile }, { ...openFile }]}
${'some files are collapsed in either way'} | ${true} | ${[{ ...manuallyCollapsedFile }, { ...autoCollapsedFile }, { ...openFile }]}
`('`manual` is $value when $description', ({ value, files }) => {
localState.diffFiles = files;
const getterResult = getters.whichCollapsedTypes(localState);
expect(getters.hasCollapsedFile(localState)).toEqual(true);
expect(getterResult.manual).toEqual(value);
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment