642 lines
20 KiB
JavaScript
642 lines
20 KiB
JavaScript
import { shallowMount } from '@vue/test-utils';
|
|
import Vue, { nextTick } from 'vue';
|
|
import { cloneDeep } from 'lodash';
|
|
import Vuex from 'vuex';
|
|
|
|
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
|
|
|
|
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
|
|
import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE } from '~/diffs/constants';
|
|
import { reviewFile } from '~/diffs/store/actions';
|
|
import { SET_DIFF_FILE_VIEWED, SET_MR_FILE_REVIEWS } from '~/diffs/store/mutation_types';
|
|
import { diffViewerModes } from '~/ide/constants';
|
|
import { scrollToElement } from '~/lib/utils/common_utils';
|
|
import { truncateSha } from '~/lib/utils/text_utility';
|
|
import { __, sprintf } from '~/locale';
|
|
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
|
|
|
import testAction from '../../__helpers__/vuex_action_helper';
|
|
import diffDiscussionsMockData from '../mock_data/diff_discussions';
|
|
|
|
jest.mock('~/lib/utils/common_utils');
|
|
|
|
const diffFile = Object.freeze(
|
|
Object.assign(diffDiscussionsMockData.diff_file, {
|
|
id: '123',
|
|
file_hash: 'xyz',
|
|
file_identifier_hash: 'abc',
|
|
edit_path: 'link:/to/edit/path',
|
|
blob: {
|
|
id: '848ed9407c6730ff16edb3dd24485a0eea24292a',
|
|
path: 'lib/base.js',
|
|
name: 'base.js',
|
|
mode: '100644',
|
|
readable_text: true,
|
|
icon: 'doc-text',
|
|
},
|
|
}),
|
|
);
|
|
|
|
Vue.use(Vuex);
|
|
|
|
describe('DiffFileHeader component', () => {
|
|
let wrapper;
|
|
let mockStoreConfig;
|
|
|
|
const diffHasExpandedDiscussionsResultMock = jest.fn();
|
|
const diffHasDiscussionsResultMock = jest.fn();
|
|
const defaultMockStoreConfig = {
|
|
state: {},
|
|
modules: {
|
|
diffs: {
|
|
namespaced: true,
|
|
getters: {
|
|
diffHasExpandedDiscussions: () => diffHasExpandedDiscussionsResultMock,
|
|
diffHasDiscussions: () => diffHasDiscussionsResultMock,
|
|
},
|
|
actions: {
|
|
toggleFileDiscussions: jest.fn(),
|
|
toggleFileDiscussionWrappers: jest.fn(),
|
|
toggleFullDiff: jest.fn(),
|
|
setCurrentFileHash: jest.fn(),
|
|
setFileCollapsedByUser: jest.fn(),
|
|
reviewFile: jest.fn(),
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
afterEach(() => {
|
|
[
|
|
diffHasDiscussionsResultMock,
|
|
diffHasExpandedDiscussionsResultMock,
|
|
...Object.values(mockStoreConfig.modules.diffs.actions),
|
|
].forEach((mock) => mock.mockReset());
|
|
|
|
wrapper.destroy();
|
|
});
|
|
|
|
const findHeader = () => wrapper.findComponent({ ref: 'header' });
|
|
const findTitleLink = () => wrapper.findComponent({ ref: 'titleWrapper' });
|
|
const findExpandButton = () => wrapper.findComponent({ ref: 'expandDiffToFullFileButton' });
|
|
const findFileActions = () => wrapper.find('.file-actions');
|
|
const findModeChangedLine = () => wrapper.findComponent({ ref: 'fileMode' });
|
|
const findLfsLabel = () => wrapper.find('[data-testid="label-lfs"]');
|
|
const findToggleDiscussionsButton = () =>
|
|
wrapper.findComponent({ ref: 'toggleDiscussionsButton' });
|
|
const findExternalLink = () => wrapper.findComponent({ ref: 'externalLink' });
|
|
const findReplacedFileButton = () => wrapper.findComponent({ ref: 'replacedFileButton' });
|
|
const findViewFileButton = () => wrapper.findComponent({ ref: 'viewButton' });
|
|
const findCollapseIcon = () => wrapper.findComponent({ ref: 'collapseIcon' });
|
|
const findEditButton = () => wrapper.findComponent({ ref: 'editButton' });
|
|
const findReviewFileCheckbox = () => wrapper.find("[data-testid='fileReviewCheckbox']");
|
|
|
|
const createComponent = ({ props, options = {} } = {}) => {
|
|
mockStoreConfig = cloneDeep(defaultMockStoreConfig);
|
|
const store = new Vuex.Store({ ...mockStoreConfig, ...(options.store || {}) });
|
|
|
|
wrapper = shallowMount(DiffFileHeader, {
|
|
propsData: {
|
|
diffFile,
|
|
canCurrentUserFork: false,
|
|
viewDiffsFileByFile: false,
|
|
...props,
|
|
},
|
|
...options,
|
|
store,
|
|
});
|
|
};
|
|
|
|
it.each`
|
|
visibility | collapsible
|
|
${'visible'} | ${true}
|
|
${'hidden'} | ${false}
|
|
`('collapse toggle is $visibility if collapsible is $collapsible', ({ collapsible }) => {
|
|
createComponent({ props: { collapsible } });
|
|
expect(findCollapseIcon().exists()).toBe(collapsible);
|
|
});
|
|
|
|
it.each`
|
|
expanded | icon
|
|
${true} | ${'chevron-down'}
|
|
${false} | ${'chevron-right'}
|
|
`('collapse icon is $icon if expanded is $expanded', ({ icon, expanded }) => {
|
|
createComponent({ props: { expanded, collapsible: true } });
|
|
expect(findCollapseIcon().props('name')).toBe(icon);
|
|
});
|
|
|
|
it('when header is clicked emits toggleFile', async () => {
|
|
createComponent();
|
|
findHeader().trigger('click');
|
|
|
|
await nextTick();
|
|
expect(wrapper.emitted().toggleFile).toBeDefined();
|
|
});
|
|
|
|
it('when collapseIcon is clicked emits toggleFile', async () => {
|
|
createComponent({ props: { collapsible: true } });
|
|
findCollapseIcon().vm.$emit('click', new Event('click'));
|
|
await nextTick();
|
|
expect(wrapper.emitted().toggleFile).toBeDefined();
|
|
});
|
|
|
|
it('when other element in header is clicked does not emits toggleFile', async () => {
|
|
createComponent({ props: { collapsible: true } });
|
|
findTitleLink().trigger('click');
|
|
|
|
await nextTick();
|
|
expect(wrapper.emitted().toggleFile).not.toBeDefined();
|
|
});
|
|
|
|
describe('copy to clipboard', () => {
|
|
beforeEach(() => {
|
|
createComponent();
|
|
});
|
|
|
|
it('displays a copy to clipboard button', () => {
|
|
expect(wrapper.findComponent(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,
|
|
submodule: true,
|
|
submodule_link: 'link://to/submodule',
|
|
};
|
|
|
|
it('prefers submodule_tree_url over submodule_link for href', () => {
|
|
const submoduleTreeUrl = 'some://tree/url';
|
|
createComponent({
|
|
props: {
|
|
discussionLink: 'discussionLink',
|
|
diffFile: {
|
|
...submoduleDiffFile,
|
|
submodule_tree_url: 'some://tree/url',
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(findTitleLink().attributes('href')).toBe(submoduleTreeUrl);
|
|
});
|
|
|
|
it('uses submodule_link for href if submodule_tree_url does not exists', () => {
|
|
const submoduleLink = 'link://to/submodule';
|
|
createComponent({
|
|
props: {
|
|
discussionLink: 'discussionLink',
|
|
diffFile: submoduleDiffFile,
|
|
},
|
|
});
|
|
|
|
expect(findTitleLink().attributes('href')).toBe(submoduleLink);
|
|
});
|
|
|
|
it('uses file_path + SHA as link text', () => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: submoduleDiffFile,
|
|
},
|
|
});
|
|
|
|
expect(findTitleLink().text()).toContain(
|
|
`${diffFile.file_path} @ ${truncateSha(diffFile.blob.id)}`,
|
|
);
|
|
});
|
|
|
|
it('does not render file actions', () => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: submoduleDiffFile,
|
|
addMergeRequestButtons: true,
|
|
},
|
|
});
|
|
expect(findFileActions().exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('for any file', () => {
|
|
const allModes = Object.keys(diffViewerModes).map((m) => [m]);
|
|
|
|
it.each(allModes)('for %s file mode displays mode changes', (mode) => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: {
|
|
...diffFile,
|
|
mode_changed: true,
|
|
a_mode: 'old-mode',
|
|
b_mode: 'new-mode',
|
|
viewer: {
|
|
...diffFile.viewer,
|
|
name: diffViewerModes[mode],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(findModeChangedLine().text()).toMatch(/old-mode.+new-mode/);
|
|
});
|
|
|
|
it.each(allModes.filter((m) => m[0] !== 'mode_changed'))(
|
|
'for %s file mode does not display mode changes',
|
|
(mode) => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: {
|
|
...diffFile,
|
|
mode_changed: false,
|
|
a_mode: 'old-mode',
|
|
b_mode: 'new-mode',
|
|
viewer: {
|
|
...diffFile.viewer,
|
|
name: diffViewerModes[mode],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(findModeChangedLine().exists()).toBe(false);
|
|
},
|
|
);
|
|
|
|
it('displays the LFS label for files stored in LFS', () => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: { ...diffFile, stored_externally: true, external_storage: 'lfs' },
|
|
},
|
|
});
|
|
expect(findLfsLabel().exists()).toBe(true);
|
|
});
|
|
|
|
it('does not display the LFS label for files stored in repository', () => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: { ...diffFile, stored_externally: false },
|
|
},
|
|
});
|
|
expect(findLfsLabel().exists()).toBe(false);
|
|
});
|
|
|
|
it('does not render view replaced file button if no replaced view path is present', () => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: { ...diffFile, replaced_view_path: null },
|
|
},
|
|
});
|
|
expect(findReplacedFileButton().exists()).toBe(false);
|
|
});
|
|
|
|
describe('when addMergeRequestButtons is false', () => {
|
|
it('does not render file actions', () => {
|
|
createComponent({ props: { addMergeRequestButtons: false } });
|
|
expect(findFileActions().exists()).toBe(false);
|
|
});
|
|
it('should not render edit button', () => {
|
|
createComponent({ props: { addMergeRequestButtons: false } });
|
|
expect(findEditButton().exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('when addMergeRequestButtons is true', () => {
|
|
describe('without discussions', () => {
|
|
it('does not render a toggle discussions button', () => {
|
|
diffHasDiscussionsResultMock.mockReturnValue(false);
|
|
createComponent({ props: { addMergeRequestButtons: true } });
|
|
expect(findToggleDiscussionsButton().exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('with discussions', () => {
|
|
it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => {
|
|
diffHasDiscussionsResultMock.mockReturnValue(true);
|
|
createComponent({ props: { addMergeRequestButtons: true } });
|
|
expect(findToggleDiscussionsButton().exists()).toBe(true);
|
|
findToggleDiscussionsButton().vm.$emit('click');
|
|
expect(
|
|
mockStoreConfig.modules.diffs.actions.toggleFileDiscussionWrappers,
|
|
).toHaveBeenCalledWith(expect.any(Object), diffFile);
|
|
});
|
|
});
|
|
|
|
it('should show edit button', () => {
|
|
createComponent({
|
|
props: {
|
|
addMergeRequestButtons: true,
|
|
},
|
|
});
|
|
expect(findEditButton().exists()).toBe(true);
|
|
});
|
|
|
|
describe('view on environment button', () => {
|
|
it('is displayed when external url is provided', () => {
|
|
const externalUrl = 'link://to/external';
|
|
const formattedExternalUrl = 'link://formatted';
|
|
createComponent({
|
|
props: {
|
|
diffFile: {
|
|
...diffFile,
|
|
external_url: externalUrl,
|
|
formatted_external_url: formattedExternalUrl,
|
|
},
|
|
addMergeRequestButtons: true,
|
|
},
|
|
});
|
|
expect(findExternalLink().exists()).toBe(true);
|
|
});
|
|
|
|
it('is hidden by default', () => {
|
|
createComponent({ props: { addMergeRequestButtons: true } });
|
|
expect(findExternalLink().exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('without file blob', () => {
|
|
beforeEach(() => {
|
|
createComponent({ props: { diffFile: { ...diffFile, blob: false } } });
|
|
});
|
|
|
|
it('should not render toggle discussions button', () => {
|
|
expect(findToggleDiscussionsButton().exists()).toBe(false);
|
|
});
|
|
|
|
it('should not render edit button', () => {
|
|
expect(findEditButton().exists()).toBe(false);
|
|
});
|
|
});
|
|
describe('with file blob', () => {
|
|
it('should render correct file view button', () => {
|
|
const viewPath = 'link://view-path';
|
|
createComponent({
|
|
props: {
|
|
diffFile: { ...diffFile, view_path: viewPath },
|
|
addMergeRequestButtons: true,
|
|
},
|
|
});
|
|
expect(findViewFileButton().attributes('href')).toBe(viewPath);
|
|
expect(findViewFileButton().text()).toEqual(
|
|
`View file @ ${diffFile.content_sha.substr(0, 8)}`,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('expand full file button', () => {
|
|
describe('when diff is fully expanded', () => {
|
|
it('is not rendered', () => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: {
|
|
...diffFile,
|
|
is_fully_expanded: true,
|
|
},
|
|
},
|
|
});
|
|
expect(findExpandButton().exists()).toBe(false);
|
|
});
|
|
});
|
|
describe('when diff is not fully expanded', () => {
|
|
const fullyNotExpandedFileProps = {
|
|
diffFile: {
|
|
...diffFile,
|
|
is_fully_expanded: false,
|
|
edit_path: 'link/to/edit/path.txt',
|
|
isShowingFullFile: false,
|
|
},
|
|
addMergeRequestButtons: true,
|
|
};
|
|
|
|
it('renders expand to full file button if not showing full file already', () => {
|
|
createComponent({ props: fullyNotExpandedFileProps });
|
|
expect(findExpandButton().exists()).toBe(true);
|
|
});
|
|
|
|
it('renders loading icon when loading full file', () => {
|
|
createComponent({ props: fullyNotExpandedFileProps });
|
|
expect(findExpandButton().exists()).toBe(true);
|
|
});
|
|
|
|
it('toggles full diff on click', () => {
|
|
createComponent({ props: fullyNotExpandedFileProps });
|
|
findExpandButton().vm.$emit('click');
|
|
expect(mockStoreConfig.modules.diffs.actions.toggleFullDiff).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('uses discussionPath for link if it is defined', () => {
|
|
const discussionPath = 'link://to/discussion';
|
|
createComponent({
|
|
props: {
|
|
discussionPath,
|
|
},
|
|
});
|
|
expect(findTitleLink().attributes('href')).toBe(discussionPath);
|
|
});
|
|
|
|
it('uses local anchor for link as last resort', () => {
|
|
createComponent();
|
|
expect(findTitleLink().attributes('href')).toMatch(/^#diff-content/);
|
|
});
|
|
|
|
describe('when local anchor for link is clicked', () => {
|
|
beforeEach(() => {
|
|
createComponent();
|
|
});
|
|
|
|
it('scrolls to target', () => {
|
|
findTitleLink().trigger('click');
|
|
expect(scrollToElement).toHaveBeenCalled();
|
|
});
|
|
|
|
it('updates anchor in URL', () => {
|
|
findTitleLink().trigger('click');
|
|
expect(window.location.href).toMatch(/#diff-content/);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('for new file', () => {
|
|
it('displays the path', () => {
|
|
createComponent({ props: { diffFile: { ...diffFile, new_file: true } } });
|
|
expect(findTitleLink().text()).toBe(diffFile.file_path);
|
|
});
|
|
});
|
|
|
|
describe('for deleted file', () => {
|
|
it('displays the path', () => {
|
|
createComponent({ props: { diffFile: { ...diffFile, deleted_file: true } } });
|
|
expect(findTitleLink().text()).toBe(
|
|
sprintf(__('%{filePath} deleted'), { filePath: diffFile.file_path }, false),
|
|
);
|
|
});
|
|
|
|
it('does not show edit button', () => {
|
|
createComponent({ props: { diffFile: { ...diffFile, deleted_file: true } } });
|
|
expect(findEditButton().exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('for renamed file', () => {
|
|
it('displays old and new path if the file was renamed', () => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: {
|
|
...diffFile,
|
|
renamed_file: true,
|
|
old_path_html: 'old',
|
|
new_path_html: 'new',
|
|
},
|
|
},
|
|
});
|
|
expect(findTitleLink().text()).toMatch(/^old.+new/s);
|
|
});
|
|
});
|
|
|
|
describe('for replaced file', () => {
|
|
it('renders view replaced file button', () => {
|
|
const replacedViewPath = 'some/path';
|
|
createComponent({
|
|
props: {
|
|
diffFile: {
|
|
...diffFile,
|
|
replaced_view_path: replacedViewPath,
|
|
},
|
|
addMergeRequestButtons: true,
|
|
},
|
|
});
|
|
expect(findReplacedFileButton().exists()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('file reviews', () => {
|
|
it('calls the action to set the new review', () => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: {
|
|
...diffFile,
|
|
viewer: {
|
|
...diffFile.viewer,
|
|
automaticallyCollapsed: false,
|
|
manuallyCollapsed: null,
|
|
},
|
|
},
|
|
showLocalFileReviews: true,
|
|
addMergeRequestButtons: true,
|
|
},
|
|
});
|
|
|
|
const file = wrapper.vm.diffFile;
|
|
|
|
findReviewFileCheckbox().vm.$emit('change', true);
|
|
|
|
return testAction(
|
|
reviewFile,
|
|
{ file, reviewed: true },
|
|
{},
|
|
[
|
|
{ type: SET_DIFF_FILE_VIEWED, payload: { id: file.file_hash, seen: true } },
|
|
{
|
|
type: SET_MR_FILE_REVIEWS,
|
|
payload: { [file.file_identifier_hash]: [file.id, `hash:${file.file_hash}`] },
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it.each`
|
|
description | newReviewedStatus | collapseType | aCollapse | mCollapse | callAction
|
|
${'does nothing'} | ${true} | ${DIFF_FILE_MANUAL_COLLAPSE} | ${false} | ${true} | ${false}
|
|
${'does nothing'} | ${false} | ${DIFF_FILE_AUTOMATIC_COLLAPSE} | ${true} | ${null} | ${false}
|
|
${'does nothing'} | ${true} | ${'not collapsed'} | ${false} | ${null} | ${false}
|
|
${'does nothing'} | ${false} | ${'not collapsed'} | ${false} | ${null} | ${false}
|
|
${'collapses the file'} | ${true} | ${DIFF_FILE_AUTOMATIC_COLLAPSE} | ${true} | ${null} | ${true}
|
|
`(
|
|
"$description if the new review status is reviewed = $newReviewedStatus and the file's collapse type is collapse = $collapseType",
|
|
({ newReviewedStatus, aCollapse, mCollapse, callAction }) => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: {
|
|
...diffFile,
|
|
viewer: {
|
|
...diffFile.viewer,
|
|
automaticallyCollapsed: aCollapse,
|
|
manuallyCollapsed: mCollapse,
|
|
},
|
|
},
|
|
showLocalFileReviews: true,
|
|
addMergeRequestButtons: true,
|
|
},
|
|
});
|
|
|
|
findReviewFileCheckbox().vm.$emit('change', newReviewedStatus);
|
|
|
|
if (callAction) {
|
|
expect(mockStoreConfig.modules.diffs.actions.setFileCollapsedByUser).toHaveBeenCalled();
|
|
} else {
|
|
expect(
|
|
mockStoreConfig.modules.diffs.actions.setFileCollapsedByUser,
|
|
).not.toHaveBeenCalled();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each`
|
|
description | show | visible
|
|
${'shows'} | ${true} | ${true}
|
|
${'hides'} | ${false} | ${false}
|
|
`(
|
|
'$description the file review feature given { showLocalFileReviewsProp: $show }',
|
|
({ show, visible }) => {
|
|
createComponent({
|
|
props: {
|
|
showLocalFileReviews: show,
|
|
addMergeRequestButtons: true,
|
|
},
|
|
});
|
|
|
|
expect(findReviewFileCheckbox().exists()).toEqual(visible);
|
|
},
|
|
);
|
|
|
|
it.each`
|
|
open | status | fires
|
|
${true} | ${true} | ${true}
|
|
${false} | ${false} | ${true}
|
|
${true} | ${false} | ${false}
|
|
${false} | ${true} | ${false}
|
|
`(
|
|
'toggles appropriately when { fileExpanded: $open, newReviewStatus: $status }',
|
|
({ open, status, fires }) => {
|
|
createComponent({
|
|
props: {
|
|
diffFile: {
|
|
...diffFile,
|
|
viewer: {
|
|
...diffFile.viewer,
|
|
automaticallyCollapsed: false,
|
|
manuallyCollapsed: null,
|
|
},
|
|
},
|
|
showLocalFileReviews: true,
|
|
addMergeRequestButtons: true,
|
|
expanded: open,
|
|
},
|
|
});
|
|
|
|
findReviewFileCheckbox().vm.$emit('change', status);
|
|
|
|
expect(Boolean(wrapper.emitted().toggleFile)).toBe(fires);
|
|
},
|
|
);
|
|
});
|
|
});
|