spec/frontend/diffs/stores/legacy_diffs/actions_spec.js (2,226 lines of code) (raw):
import MockAdapter from 'axios-mock-adapter';
import { createTestingPinia } from '@pinia/testing';
import api from '~/api';
import Cookies from '~/lib/utils/cookies';
import waitForPromises from 'helpers/wait_for_promises';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { getDiffFileMock } from 'jest/diffs/mock_data/diff_file';
import {
DIFF_VIEW_COOKIE_NAME,
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
EVT_MR_PREPARED,
FILE_DIFF_POSITION_TYPE,
} from '~/diffs/constants';
import {
BUILDING_YOUR_MR,
SOMETHING_WENT_WRONG,
ENCODED_FILE_PATHS_TITLE,
ENCODED_FILE_PATHS_MESSAGE,
} from '~/diffs/i18n';
import { createTestPiniaAction, createCustomGetters } from 'helpers/pinia_helpers';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
import * as treeWorkerUtils from '~/diffs/utils/tree_worker_utils';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NOT_FOUND,
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import diffsEventHub from '~/diffs/event_hub';
import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { useNotes } from '~/notes/store/legacy_notes';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { diffMetadata } from '../../mock_data/diff_metadata';
jest.mock('~/alert');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
confirmAction.mockResolvedValueOnce(false);
const endpointDiffForPath = '/diffs/set/endpoint/path';
describe('legacyDiffs actions', () => {
let getters = {};
let notesGetters = {};
let store;
let mock;
let testAction;
useLocalStorageSpy();
const originalMethods = {
requestAnimationFrame: global.requestAnimationFrame,
requestIdleCallback: global.requestIdleCallback,
};
beforeEach(() => {
getters = {};
createTestingPinia({
stubActions: false,
plugins: [
createCustomGetters(() => ({
legacyDiffs: getters,
legacyNotes: notesGetters,
batchComments: {},
})),
globalAccessorPlugin,
],
});
store = useLegacyDiffs();
testAction = createTestPiniaAction(store);
useNotes().$reset();
mock = new MockAdapter(axios);
jest.spyOn(window.history, 'pushState');
jest.spyOn(commonUtils, 'historyPushState');
jest.spyOn(commonUtils, 'handleLocationHash').mockImplementation(() => null);
jest.spyOn(commonUtils, 'scrollToElement').mockImplementation(() => null);
jest.spyOn(utils, 'convertExpandLines').mockImplementation(() => null);
jest.spyOn(utils, 'idleCallback').mockImplementation(() => null);
['requestAnimationFrame', 'requestIdleCallback'].forEach((method) => {
global[method] = (cb) => {
cb({ timeRemaining: () => 10 });
};
});
});
afterEach(() => {
['requestAnimationFrame', 'requestIdleCallback'].forEach((method) => {
global[method] = originalMethods[method];
});
createAlert.mockClear();
mock.restore();
});
describe('setBaseConfig', () => {
it('should set given endpoint and project path', () => {
const endpoint = '/diffs/set/endpoint';
const endpointMetadata = '/diffs/set/endpoint/metadata';
const endpointBatch = '/diffs/set/endpoint/batch';
const endpointCoverage = '/diffs/set/coverage_reports';
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
const showSuggestPopover = false;
const mrReviews = {
a: ['z', 'hash:a'],
b: ['y', 'hash:a'],
};
const diffViewType = 'inline';
return testAction(
store.setBaseConfig,
{
endpoint,
endpointBatch,
endpointDiffForPath,
endpointMetadata,
endpointCoverage,
projectPath,
dismissEndpoint,
showSuggestPopover,
mrReviews,
diffViewType,
},
{
endpoint: '',
endpointBatch: '',
endpointDiffForPath: '',
endpointMetadata: '',
endpointCoverage: '',
projectPath: '',
dismissEndpoint: '',
showSuggestPopover: true,
},
[
{
type: store[types.SET_BASE_CONFIG],
payload: {
endpoint,
endpointMetadata,
endpointBatch,
endpointDiffForPath,
endpointCoverage,
projectPath,
dismissEndpoint,
showSuggestPopover,
mrReviews,
diffViewType,
},
},
{
type: store[types.SET_DIFF_FILE_VIEWED],
payload: { id: 'z', seen: true },
},
{
type: store[types.SET_DIFF_FILE_VIEWED],
payload: { id: 'a', seen: true },
},
{
type: store[types.SET_DIFF_FILE_VIEWED],
payload: { id: 'y', seen: true },
},
],
[],
);
});
});
describe('prefetchSingleFile', () => {
beforeEach(() => {
window.location.hash = 'e334a2a10f036c00151a04cea7938a5d4213a818';
});
it('should do nothing if the tree entry is already loading', () => {
return testAction(store.prefetchSingleFile, { diffLoading: true }, {}, [], []);
});
it('should do nothing if the tree entry has already been marked as loaded', () => {
return testAction(
store.prefetchSingleFile,
{ diffLoaded: true },
{
flatBlobsList: [
{ fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818', diffLoaded: true },
],
},
[],
[],
);
});
describe('when a tree entry exists for the file, but it has not been marked as loaded', () => {
let state;
let hubSpy;
const defaultParams = {
old_path: 'old/123',
new_path: 'new/123',
w: '1',
view: 'inline',
diff_head: true,
};
const diffForPath = mergeUrlParams(defaultParams, endpointDiffForPath);
const treeEntry = {
fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818',
filePaths: { old: 'old/123', new: 'new/123' },
};
const fileResult = {
diff_files: [{ file_hash: 'e334a2a10f036c00151a04cea7938a5d4213a818' }],
};
beforeEach(() => {
state = {
endpointDiffForPath,
showWhitespace: false,
treeEntries: {
[treeEntry.filePaths.new]: {},
},
diffFiles: [],
};
getters = {
flatBlobsList: [treeEntry],
};
store.$patch(state);
hubSpy = jest.spyOn(diffsEventHub, '$emit');
});
it('does nothing if the file already exists in the loaded diff files', () => {
store.$patch({ diffFiles: fileResult.diff_files });
return testAction(store.prefetchSingleFile, treeEntry, getters, [], []);
});
it('does some standard work every time', async () => {
mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
await store.prefetchSingleFile(treeEntry);
expect(store[types.TREE_ENTRY_DIFF_LOADING]).toHaveBeenCalledWith({
path: treeEntry.filePaths.new,
});
// wait for the mocked network request to return
await waitForPromises();
expect(store[types.SET_DIFF_DATA_BATCH].mock.calls[0]).toMatchObject([fileResult]);
expect(hubSpy).toHaveBeenCalledWith('diffFilesModified');
});
it('should fetch data without commit ID', async () => {
getters.commitId = null;
mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
await store.prefetchSingleFile(treeEntry);
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
// This tests that commit_id is NOT added, if there isn't one in the store
expect(mock.history.get[0].url).toEqual(diffForPath);
});
it('should fetch data with commit ID', async () => {
const finalPath = mergeUrlParams(
{ ...defaultParams, commit_id: '123' },
endpointDiffForPath,
);
getters.commitId = '123';
mock.onGet(finalPath).reply(HTTP_STATUS_OK, fileResult);
await store.prefetchSingleFile(treeEntry);
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
expect(mock.history.get[0].url).toContain(
'old_path=old%2F123&new_path=new%2F123&w=1&view=inline&commit_id=123',
);
});
describe('version parameters', () => {
const diffId = '4';
const startSha = 'abc';
const pathRoot = 'a/a/-/merge_requests/1';
it('fetches the data when there is no mergeRequestDiff', async () => {
store.prefetchSingleFile(treeEntry);
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
expect(mock.history.get[0].url).toEqual(diffForPath);
});
it.each`
desc | versionPath | start_sha | diff_id
${'no additional version information'} | ${`${pathRoot}?search=terms`} | ${undefined} | ${undefined}
${'the diff_id'} | ${`${pathRoot}?diff_id=${diffId}`} | ${undefined} | ${diffId}
${'the start_sha'} | ${`${pathRoot}?start_sha=${startSha}`} | ${startSha} | ${undefined}
${'all available version information'} | ${`${pathRoot}?diff_id=${diffId}&start_sha=${startSha}`} | ${startSha} | ${diffId}
`('fetches the data and includes $desc', async ({ versionPath, start_sha, diff_id }) => {
const finalPath = mergeUrlParams(
{ ...defaultParams, diff_id, start_sha },
endpointDiffForPath,
);
store.$patch({
mergeRequestDiff: { version_path: versionPath },
endpointBatch: versionPath,
});
mock.onGet(finalPath).reply(HTTP_STATUS_OK, fileResult);
store.prefetchSingleFile(treeEntry);
// wait for the mocked network request to return
await waitForPromises();
expect(mock.history.get[0].url).toEqual(finalPath);
});
});
describe('when the prefetch fails', () => {
beforeEach(() => {
mock.onGet(diffForPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('should commit a mutation to set the tree entry diff loading to false', async () => {
store.prefetchSingleFile(treeEntry);
// wait for the mocked network request to return
await waitForPromises();
expect(store[types.TREE_ENTRY_DIFF_LOADING]).toHaveBeenCalledWith({
path: treeEntry.filePaths.new,
loading: false,
});
});
});
});
});
describe('fetchFileByFile', () => {
beforeEach(() => {
window.location.hash = 'e334a2a10f036c00151a04cea7938a5d4213a818';
});
it('should do nothing if there is no tree entry for the file ID', () => {
return testAction(store.fetchFileByFile, {}, { flatBlobsList: [] }, [], []);
});
it('should do nothing if the tree entry for the file ID has already been marked as loaded', () => {
return testAction(
store.fetchFileByFile,
{},
{
flatBlobsList: [
{ fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818', diffLoaded: true },
],
},
[],
[],
);
});
describe('when a tree entry exists for the file, but it has not been marked as loaded', () => {
let state;
let hubSpy;
const defaultParams = {
old_path: 'old/123',
new_path: 'new/123',
w: '1',
view: 'inline',
diff_head: true,
};
const diffForPath = mergeUrlParams(defaultParams, endpointDiffForPath);
const treeEntry = {
fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818',
filePaths: { old: 'old/123', new: 'new/123' },
};
const fileResult = {
diff_files: [{ file_hash: 'e334a2a10f036c00151a04cea7938a5d4213a818' }],
};
beforeEach(() => {
state = {
endpointDiffForPath,
showWhitespace: false,
diffFiles: [],
};
getters = {
flatBlobsList: [treeEntry],
};
store.$patch(state);
hubSpy = jest.spyOn(diffsEventHub, '$emit');
});
it('does nothing if the file already exists in the loaded diff files', () => {
store.$patch({ diffFiles: fileResult.diff_files });
return testAction(store.fetchFileByFile, state, {}, [], []);
});
it('does some standard work every time', async () => {
mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
await store.fetchFileByFile();
expect(store[types.SET_BATCH_LOADING_STATE]).toHaveBeenCalledWith('loading');
expect(store[types.SET_RETRIEVING_BATCHES]).toHaveBeenCalledWith(true);
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
expect(store[types.SET_DIFF_DATA_BATCH].mock.calls[0]).toMatchObject([fileResult]);
expect(store[types.SET_BATCH_LOADING_STATE]).toHaveBeenCalledWith('loaded');
expect(hubSpy).toHaveBeenCalledWith('diffFilesModified');
});
it.each`
urlHash | diffFiles | expected
${treeEntry.fileHash} | ${[]} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
${'abcdef1234567890'} | ${fileResult.diff_files} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
`(
"sets the current file to the first diff file ('$id') if it's not a note hash and there isn't a current ID set",
async ({ urlHash, diffFiles, expected }) => {
window.location.hash = urlHash;
mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
store.$patch({ diffFiles });
await store.fetchFileByFile();
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
expect(store[types.SET_CURRENT_DIFF_FILE]).toHaveBeenCalledWith(expected);
},
);
it('should fetch data without commit ID', async () => {
store.$patch({ commitId: null });
mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
await store.fetchFileByFile();
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
// This tests that commit_id is NOT added, if there isn't one in the store
expect(mock.history.get[0].url).toEqual(diffForPath);
});
it('should fetch data with commit ID', async () => {
const finalPath = mergeUrlParams(
{ ...defaultParams, commit_id: '123' },
endpointDiffForPath,
);
store.$patch({ commit: { id: '123' } });
mock.onGet(finalPath).reply(HTTP_STATUS_OK, fileResult);
await store.fetchFileByFile();
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
expect(mock.history.get[0].url).toContain(
'old_path=old%2F123&new_path=new%2F123&w=1&view=inline&commit_id=123',
);
});
describe('version parameters', () => {
const diffId = '4';
const startSha = 'abc';
const pathRoot = 'a/a/-/merge_requests/1';
it('fetches the data when there is no mergeRequestDiff', async () => {
store.fetchFileByFile();
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
expect(mock.history.get[0].url).toEqual(diffForPath);
});
it.each`
desc | versionPath | start_sha | diff_id
${'no additional version information'} | ${`${pathRoot}?search=terms`} | ${undefined} | ${undefined}
${'the diff_id'} | ${`${pathRoot}?diff_id=${diffId}`} | ${undefined} | ${diffId}
${'the start_sha'} | ${`${pathRoot}?start_sha=${startSha}`} | ${startSha} | ${undefined}
${'all available version information'} | ${`${pathRoot}?diff_id=${diffId}&start_sha=${startSha}`} | ${startSha} | ${diffId}
`('fetches the data and includes $desc', async ({ versionPath, start_sha, diff_id }) => {
const finalPath = mergeUrlParams(
{ ...defaultParams, diff_id, start_sha },
endpointDiffForPath,
);
store.$patch({ endpointBatch: versionPath });
mock.onGet(finalPath).reply(HTTP_STATUS_OK, fileResult);
store.fetchFileByFile();
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
expect(mock.history.get[0].url).toEqual(finalPath);
});
});
});
});
describe('fetchDiffFilesBatch', () => {
it('should fetch batch diff files', () => {
const endpointBatch = '/fetch/diffs_batch';
const res1 = { diff_files: [{ file_hash: 'test' }], pagination: { total_pages: 2 } };
const res2 = { diff_files: [{ file_hash: 'test2' }], pagination: { total_pages: 2 } };
mock
.onGet(
mergeUrlParams(
{
w: '1',
view: 'inline',
page: 0,
per_page: 5,
},
endpointBatch,
),
)
.reply(HTTP_STATUS_OK, res1)
.onGet(
mergeUrlParams(
{
w: '1',
view: 'inline',
page: 5,
per_page: 7,
},
endpointBatch,
),
)
.reply(HTTP_STATUS_OK, res2);
return testAction(
store.fetchDiffFilesBatch,
undefined,
{
endpointBatch,
diffViewType: 'inline',
diffFiles: [],
perPage: 5,
showWhitespace: false,
},
[
{ type: store[types.SET_BATCH_LOADING_STATE], payload: 'loading' },
{ type: store[types.SET_RETRIEVING_BATCHES], payload: true },
{
type: store[types.SET_DIFF_DATA_BATCH],
payload: {
get diff_files() {
return [store.diffFiles[0]];
},
},
},
{ type: store[types.SET_BATCH_LOADING_STATE], payload: 'loaded' },
{ type: store[types.SET_CURRENT_DIFF_FILE], payload: 'test' },
{
type: store[types.SET_DIFF_DATA_BATCH],
payload: {
get diff_files() {
return [store.diffFiles[1]];
},
},
},
{ type: store[types.SET_BATCH_LOADING_STATE], payload: 'loaded' },
{ type: store[types.SET_RETRIEVING_BATCHES], payload: false },
],
[],
);
});
});
describe('fetchDiffFilesMeta', () => {
const endpointMetadata = '/fetch/diffs_metadata.json?view=inline&w=0';
const noFilesData = { ...diffMetadata };
beforeEach(() => {
delete noFilesData.diff_files;
});
it('should fetch diff meta information', () => {
mock.onGet(endpointMetadata).reply(HTTP_STATUS_OK, diffMetadata);
return testAction(
store.fetchDiffFilesMeta,
{},
{ endpointMetadata, diffViewType: 'inline', showWhitespace: true },
[
{ type: store[types.SET_LOADING], payload: true },
{ type: store[types.SET_LOADING], payload: false },
{ type: store[types.SET_MERGE_REQUEST_DIFFS], payload: diffMetadata.merge_request_diffs },
{ type: store[types.SET_DIFF_METADATA], payload: noFilesData },
// Workers are synchronous in Jest environment (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58805)
{
type: store[types.SET_TREE_DATA],
payload: treeWorkerUtils.generateTreeList(diffMetadata.diff_files),
},
],
[],
);
});
describe('when diff metadata returns has_encoded_file_paths as true', () => {
beforeEach(() => {
mock
.onGet(endpointMetadata)
.reply(HTTP_STATUS_OK, { ...diffMetadata, has_encoded_file_paths: true });
});
it('should show a non-dismissible alert', async () => {
await testAction(
store.fetchDiffFilesMeta,
{},
{ endpointMetadata, diffViewType: 'inline', showWhitespace: true },
[
{ type: store[types.SET_LOADING], payload: true },
{ type: store[types.SET_LOADING], payload: false },
{
type: store[types.SET_MERGE_REQUEST_DIFFS],
payload: diffMetadata.merge_request_diffs,
},
{
type: store[types.SET_DIFF_METADATA],
payload: { ...noFilesData, has_encoded_file_paths: true },
},
// Workers are synchronous in Jest environment (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58805)
{
type: store[types.SET_TREE_DATA],
payload: treeWorkerUtils.generateTreeList(diffMetadata.diff_files),
},
],
[],
);
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
title: ENCODED_FILE_PATHS_TITLE,
message: ENCODED_FILE_PATHS_MESSAGE,
dismissible: false,
});
});
});
describe('on a 404 response', () => {
let dismissAlert;
beforeAll(() => {
dismissAlert = jest.fn();
mock.onGet(endpointMetadata).reply(HTTP_STATUS_NOT_FOUND);
createAlert.mockImplementation(() => ({ dismiss: dismissAlert }));
});
it('should show a warning', async () => {
await testAction(
store.fetchDiffFilesMeta,
{},
{ endpointMetadata, diffViewType: 'inline', showWhitespace: true },
[{ type: store[types.SET_LOADING], payload: true }],
[],
);
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
message: BUILDING_YOUR_MR,
variant: 'warning',
});
});
it("should attempt to close the alert if the MR reports that it's been prepared", async () => {
await testAction(
store.fetchDiffFilesMeta,
{},
{ endpointMetadata, diffViewType: 'inline', showWhitespace: true },
[{ type: store[types.SET_LOADING], payload: true }],
[],
);
diffsEventHub.$emit(EVT_MR_PREPARED);
expect(dismissAlert).toHaveBeenCalled();
});
});
it('should show no warning on any other status code', async () => {
mock.onGet(endpointMetadata).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
try {
await testAction(
store.fetchDiffFilesMeta,
{},
{ endpointMetadata, diffViewType: 'inline', showWhitespace: true },
[{ type: store[types.SET_LOADING], payload: true }],
[],
);
} catch (error) {
expect(error.response.status).toBe(HTTP_STATUS_INTERNAL_SERVER_ERROR);
}
expect(createAlert).not.toHaveBeenCalled();
});
});
describe('prefetchFileNeighbors', () => {
it('dispatches two requests to prefetch the next/previous files', () => {
store.prefetchSingleFile.mockReturnValue({});
return testAction(
store.prefetchFileNeighbors,
{},
{
currentDiffFileId: 'ghi',
treeEntries: {
abc: {
type: 'blob',
fileHash: 'abc',
},
ghi: {
type: 'blob',
fileHash: 'ghi',
},
def: {
type: 'blob',
fileHash: 'def',
},
},
},
[],
[
{ type: store.prefetchSingleFile, payload: { type: 'blob', fileHash: 'def' } },
{ type: store.prefetchSingleFile, payload: { type: 'blob', fileHash: 'abc' } },
],
);
});
});
describe('fetchCoverageFiles', () => {
const endpointCoverage = '/fetch';
it('should commit SET_COVERAGE_DATA with received response', () => {
const data = { files: { 'app.js': { 1: 0, 2: 1 } } };
mock.onGet(endpointCoverage).reply(HTTP_STATUS_OK, { data });
return testAction(
store.fetchCoverageFiles,
{},
{ endpointCoverage },
[{ type: store[types.SET_COVERAGE_DATA], payload: { data } }],
[],
);
});
it('should show alert on API error', async () => {
mock.onGet(endpointCoverage).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(store.fetchCoverageFiles, {}, { endpointCoverage }, [], []);
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
message: SOMETHING_WENT_WRONG,
});
});
});
describe('setHighlightedRow', () => {
it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => {
return testAction(store.setHighlightedRow, { lineCode: 'ABC_123' }, {}, [
{ type: store[types.SET_HIGHLIGHTED_ROW], payload: 'ABC_123' },
{ type: store[types.SET_CURRENT_DIFF_FILE], payload: 'ABC' },
]);
});
it('should prevent default event', () => {
const preventDefault = jest.fn();
const target = { href: TEST_HOST };
const event = { target, preventDefault };
testAction(store.setHighlightedRow, { lineCode: 'ABC_123', event }, {}, [
{ type: store[types.SET_HIGHLIGHTED_ROW], payload: 'ABC_123' },
{ type: store[types.SET_CURRENT_DIFF_FILE], payload: 'ABC' },
]);
expect(preventDefault).toHaveBeenCalled();
});
it('should filter out linked file param', () => {
const target = { href: `${TEST_HOST}/diffs?file=foo#abc_11` };
const event = { target, preventDefault: jest.fn() };
testAction(store.setHighlightedRow, { lineCode: 'ABC_123', event }, {}, [
{ type: store[types.SET_HIGHLIGHTED_ROW], payload: 'ABC_123' },
{ type: store[types.SET_CURRENT_DIFF_FILE], payload: 'ABC' },
]);
expect(window.location.href).toBe(`${TEST_HOST}/diffs#abc_11`);
});
});
describe('assignDiscussionsToDiff', () => {
afterEach(() => {
window.location.hash = '';
});
it('should merge discussions into diffs', () => {
window.location.hash = 'ABC_123';
const state = {
diffFiles: [
{
file_hash: 'ABC',
parallel_diff_lines: [
{
left: {
line_code: 'ABC_1_1',
discussions: [],
},
right: {
line_code: 'ABC_1_1',
discussions: [],
},
},
],
highlighted_diff_lines: [
{
line_code: 'ABC_1_1',
discussions: [],
old_line: 5,
new_line: null,
},
],
diff_refs: {
base_sha: 'abc',
head_sha: 'def',
start_sha: 'ghi',
},
new_path: 'file1',
old_path: 'file2',
},
],
};
const diffPosition = {
base_sha: 'abc',
head_sha: 'def',
start_sha: 'ghi',
new_line: null,
new_path: 'file1',
old_line: 5,
old_path: 'file2',
};
const singleDiscussion = {
line_code: 'ABC_1_1',
diff_discussion: {},
diff_file: {
file_hash: 'ABC',
},
file_hash: 'ABC',
resolvable: true,
position: diffPosition,
original_position: diffPosition,
};
const discussions = [singleDiscussion];
return testAction(
store.assignDiscussionsToDiff,
discussions,
state,
[
{
type: store[types.SET_LINE_DISCUSSIONS_FOR_FILE],
payload: {
discussion: singleDiscussion,
diffPositionByLineCode: {
ABC_1_1: {
base_sha: 'abc',
head_sha: 'def',
start_sha: 'ghi',
new_line: null,
new_path: 'file1',
old_line: 5,
old_path: 'file2',
line_range: null,
line_code: 'ABC_1_1',
position_type: 'text',
},
},
hash: 'ABC_123',
},
},
],
[],
);
});
it('dispatches setCurrentDiffFileIdFromNote with note ID', () => {
window.location.hash = 'note_123';
getters = {
flatBlobsList: [],
};
notesGetters = {
notesById: {},
};
return testAction(
store.assignDiscussionsToDiff,
[],
{ diffFiles: [] },
[],
[{ type: store.setCurrentDiffFileIdFromNote, payload: '123' }],
);
});
});
describe('removeDiscussionsFromDiff', () => {
it('does not call mutation if no diff file is on discussion', () => {
testAction(
store.removeDiscussionsFromDiff,
{
id: '1',
line_code: 'ABC_1_1',
},
{},
[],
[],
);
});
it('should remove discussions from diffs', () => {
const state = {
diffFiles: [
{
file_hash: 'ABC',
parallel_diff_lines: [
{
left: {
line_code: 'ABC_1_1',
discussions: [
{
id: 1,
},
],
},
right: {
line_code: 'ABC_1_1',
discussions: [],
},
},
],
highlighted_diff_lines: [
{
line_code: 'ABC_1_1',
discussions: [],
},
],
},
],
};
const singleDiscussion = {
id: '1',
diff_file: { file_hash: 'ABC' },
line_code: 'ABC_1_1',
};
return testAction(
store.removeDiscussionsFromDiff,
singleDiscussion,
state,
[
{
type: store[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE],
payload: {
id: '1',
fileHash: 'ABC',
lineCode: 'ABC_1_1',
},
},
],
[],
);
});
});
describe('setDiffViewType', () => {
it.each([['inline'], ['parallel']])(
'should set the diff view type to $p and set the cookie',
async (diffViewType) => {
await testAction(
store.setDiffViewType,
diffViewType,
{},
[{ type: store[types.SET_DIFF_VIEW_TYPE], payload: diffViewType }],
[],
);
expect(window.location.toString()).toContain(`?view=${diffViewType}`);
expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(diffViewType);
},
);
});
describe('showCommentForm', () => {
it('should call mutation to show comment form', () => {
const payload = { lineCode: 'lineCode', fileHash: 'hash' };
return testAction(
store.showCommentForm,
payload,
{},
[{ type: store[types.TOGGLE_LINE_HAS_FORM], payload: { ...payload, hasForm: true } }],
[],
);
});
});
describe('cancelCommentForm', () => {
it('should call mutation to cancel comment form', () => {
const payload = { lineCode: 'lineCode', fileHash: 'hash' };
return testAction(
store.cancelCommentForm,
payload,
{},
[{ type: store[types.TOGGLE_LINE_HAS_FORM], payload: { ...payload, hasForm: false } }],
[],
);
});
});
describe('loadMoreLines', () => {
it('should call mutation to show comment form', () => {
const endpoint = '/diffs/load/more/lines';
const params = { since: 6, to: 26 };
const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
const fileHash = 'ff9200';
const isExpandDown = false;
const nextLineNumbers = {};
const options = { endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers };
const contextLines = [{ lineCode: 6 }];
mock.onGet(endpoint).reply(HTTP_STATUS_OK, contextLines);
return testAction(
store.loadMoreLines,
options,
{ diffFiles: [{ file_hash: fileHash, highlighted_diff_lines: [] }] },
[
{
type: store[types.ADD_CONTEXT_LINES],
payload: {
lineNumbers,
contextLines: [{ ...contextLines[0], new_line: 4, old_line: 2 }],
params,
fileHash,
isExpandDown,
nextLineNumbers,
},
},
],
[],
);
});
});
describe('loadCollapsedDiff', () => {
it('should fetch data and call mutation with response and the give parameter', async () => {
const file = {
file_hash: 'collapsed',
hash: 123,
load_collapsed_diff_url: '/load/collapsed/diff/url',
};
const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] };
mock.onGet(file.loadCollapsedDiffUrl).reply(HTTP_STATUS_OK, data);
store.$patch({
showWhitespace: true,
commit: { id: null },
diffFiles: [{ file_hash: file.file_hash }],
});
await store.loadCollapsedDiff({ file });
expect(store[types.ADD_COLLAPSED_DIFFS]).toHaveBeenCalledWith({ file, data });
});
it('should fetch data without commit ID', () => {
const file = { file_hash: 'collapsed', load_collapsed_diff_url: '/load/collapsed/diff/url' };
store.$patch({ commit: { id: null }, diffFiles: [{ file_hash: file.file_hash }] });
jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
store.loadCollapsedDiff({ file });
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
params: { commit_id: null, w: '0' },
});
});
it('should pass through params', () => {
const file = { file_hash: 'collapsed', load_collapsed_diff_url: '/load/collapsed/diff/url' };
store.$patch({ commit: { id: null }, diffFiles: [{ file_hash: file.file_hash }] });
jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
store.loadCollapsedDiff({ file, params: { w: '1' } });
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
params: { commit_id: null, w: '1' },
});
});
it('should fetch data with commit ID', () => {
const file = { file_hash: 'collapsed', load_collapsed_diff_url: '/load/collapsed/diff/url' };
store.$patch({ commit: { id: '123' }, diffFiles: [{ file_hash: file.file_hash }] });
jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
store.loadCollapsedDiff({ file });
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
params: { commit_id: '123', w: '0' },
});
});
describe('version parameters', () => {
const diffId = '4';
const startSha = 'abc';
const pathRoot = 'a/a/-/merge_requests/1';
let file;
beforeAll(() => {
file = { file_hash: 'collapsed', load_collapsed_diff_url: '/load/collapsed/diff/url' };
});
beforeEach(() => {
store.$patch({ diffFiles: [{ file_hash: file.file_hash }] });
jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
});
it('fetches the data when there is no mergeRequestDiff', () => {
store.loadCollapsedDiff({ file });
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
params: expect.any(Object),
});
});
it.each`
desc | versionPath | start_sha | diff_id
${'no additional version information'} | ${`${pathRoot}?search=terms`} | ${undefined} | ${undefined}
${'the diff_id'} | ${`${pathRoot}?diff_id=${diffId}`} | ${undefined} | ${diffId}
${'the start_sha'} | ${`${pathRoot}?start_sha=${startSha}`} | ${startSha} | ${undefined}
${'all available version information'} | ${`${pathRoot}?diff_id=${diffId}&start_sha=${startSha}`} | ${startSha} | ${diffId}
`('fetches the data and includes $desc', ({ versionPath, start_sha, diff_id }) => {
jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
store.$patch({ mergeRequestDiff: { version_path: versionPath } });
store.loadCollapsedDiff({ file });
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
params: expect.objectContaining({ start_sha, diff_id }),
});
});
});
});
describe('scrollToLineIfNeededInline', () => {
const lineMock = {
line_code: 'ABC_123',
};
it('should not call handleLocationHash when there is not hash', () => {
window.location.hash = '';
store.scrollToLineIfNeededInline(lineMock);
expect(commonUtils.handleLocationHash).not.toHaveBeenCalled();
});
it('should not call handleLocationHash when the hash does not match any line', () => {
window.location.hash = 'XYZ_456';
store.scrollToLineIfNeededInline(lineMock);
expect(commonUtils.handleLocationHash).not.toHaveBeenCalled();
});
it('should call handleLocationHash only when the hash matches a line', () => {
window.location.hash = 'ABC_123';
store.scrollToLineIfNeededInline({
lineCode: 'ABC_456',
});
store.scrollToLineIfNeededInline(lineMock);
store.scrollToLineIfNeededInline({
lineCode: 'XYZ_456',
});
expect(commonUtils.handleLocationHash).toHaveBeenCalled();
expect(commonUtils.handleLocationHash).toHaveBeenCalledTimes(1);
});
});
describe('scrollToLineIfNeededParallel', () => {
const lineMock = {
left: null,
right: {
line_code: 'ABC_123',
},
};
it('should not call handleLocationHash when there is not hash', () => {
window.location.hash = '';
store.scrollToLineIfNeededParallel(lineMock);
expect(commonUtils.handleLocationHash).not.toHaveBeenCalled();
});
it('should not call handleLocationHash when the hash does not match any line', () => {
window.location.hash = 'XYZ_456';
store.scrollToLineIfNeededParallel(lineMock);
expect(commonUtils.handleLocationHash).not.toHaveBeenCalled();
});
it('should call handleLocationHash only when the hash matches a line', () => {
window.location.hash = 'ABC_123';
store.scrollToLineIfNeededParallel({
left: null,
right: {
lineCode: 'ABC_456',
},
});
store.scrollToLineIfNeededParallel(lineMock);
store.scrollToLineIfNeededParallel({
left: null,
right: {
lineCode: 'XYZ_456',
},
});
expect(commonUtils.handleLocationHash).toHaveBeenCalled();
expect(commonUtils.handleLocationHash).toHaveBeenCalledTimes(1);
});
});
describe('saveDiffDiscussion', () => {
const endpoint = '/create-note-path';
const commitId = 'something';
const formData = {
diffFile: getDiffFileMock(),
noteableData: {
create_note_path: endpoint,
},
positionType: FILE_DIFF_POSITION_TYPE,
};
const note = 'note';
const state = {
commit: {
id: commitId,
},
};
beforeEach(() => {
store.$patch(state);
});
it('dispatches actions', async () => {
const discussion = { id: 1 };
useNotes().saveNote.mockResolvedValue(Promise.resolve({ discussion }));
useNotes().updateDiscussion.mockResolvedValue(Promise.resolve('discussion'));
useNotes().updateResolvableDiscussionsCounts.mockResolvedValue(Promise.resolve());
store.assignDiscussionsToDiff.mockResolvedValueOnce(Promise.resolve());
store.toggleFileCommentForm.mockResolvedValueOnce(Promise.resolve());
store.closeDiffFileCommentForm.mockResolvedValueOnce(Promise.resolve());
await store.saveDiffDiscussion({ note, formData });
expect(useNotes().saveNote.mock.calls[0][0]).toMatchObject({
data: {
note: {
commit_id: commitId,
note,
},
},
endpoint,
});
expect(useNotes().updateDiscussion).toHaveBeenCalledWith(discussion);
expect(useNotes().updateResolvableDiscussionsCounts).toHaveBeenCalledTimes(1);
expect(store.assignDiscussionsToDiff).toHaveBeenCalledWith(['discussion']);
expect(store.closeDiffFileCommentForm).toHaveBeenCalledWith(formData.diffFile.file_hash);
expect(store.toggleFileCommentForm).toHaveBeenCalledWith(formData.diffFile.file_path);
});
it('should not allow adding note with sensitive token', async () => {
const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
await store.saveDiffDiscussion({ note: sensitiveMessage, formData });
expect(useNotes().saveNote).not.toHaveBeenCalled();
expect(confirmAction).toHaveBeenCalledWith(
'',
expect.objectContaining({
title: 'Warning: Potential secret detected',
}),
);
});
});
describe('toggleTreeOpen', () => {
it('commits TOGGLE_FOLDER_OPEN', () => {
return testAction(
store.toggleTreeOpen,
'path',
{ treeEntries: { path: {} } },
[{ type: store[types.TOGGLE_FOLDER_OPEN], payload: 'path' }],
[],
);
});
});
describe('setTreeOpen', () => {
it('commits SET_FOLDER_OPEN', () => {
return testAction(
store.setTreeOpen,
{ path: 'path', opened: true },
{ treeEntries: { path: {} } },
[{ type: store[types.SET_FOLDER_OPEN], payload: { path: 'path', opened: true } }],
[],
);
});
});
describe('goToFile', () => {
const file = { path: 'path' };
const fileHash = 'test';
let state;
beforeEach(() => {
getters.isTreePathLoaded = () => false;
state = {
viewDiffsFileByFile: true,
treeEntries: {
path: {
fileHash,
},
},
};
store.$patch(state);
});
it('immediately defers to scrollToFile if the app is not in file-by-file mode', () => {
store.$patch({ viewDiffsFileByFile: false });
store.goToFile(file);
expect(store.scrollToFile).toHaveBeenCalledWith(file);
});
describe('when the app is in fileByFile mode', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
store.goToFile(file);
expect(store[types.SET_CURRENT_DIFF_FILE]).toHaveBeenCalledWith(fileHash);
});
it('does nothing more if the path has already been loaded', () => {
getters.isTreePathLoaded = () => true;
store.goToFile(file);
expect(store[types.SET_CURRENT_DIFF_FILE]).toHaveBeenCalledWith(fileHash);
expect(store.fetchFileByFile).not.toHaveBeenCalled();
});
describe('when the tree entry has not been loaded', () => {
it('updates location hash', () => {
store.goToFile(file);
expect(historyPushState).toHaveBeenCalledWith(new URL(`${TEST_HOST}#test`), {
skipScrolling: true,
});
expect(scrollToElement).toHaveBeenCalledWith('.diff-files-holder', { duration: 0 });
});
it('loads the file and then scrolls to it', async () => {
store.goToFile(file);
// Wait for the fetchFileByFile dispatch to return, to trigger scrollToFile
await waitForPromises();
expect(store.fetchFileByFile).toHaveBeenCalled();
expect(commonUtils.historyPushState).toHaveBeenCalledWith(new URL(`${TEST_HOST}/#test`), {
skipScrolling: true,
});
expect(commonUtils.scrollToElement).toHaveBeenCalledWith('.diff-files-holder', {
duration: 0,
});
expect(store.fetchFileByFile).toHaveBeenCalledWith();
});
it('unlinks the file', () => {
store.goToFile(file);
expect(store.unlinkFile).toHaveBeenCalledWith();
});
});
});
});
describe('scrollToFile', () => {
beforeEach(() => {
store.virtualScrollerDisabled = true;
});
it('updates location hash', () => {
const state = {
treeEntries: {
path: {
fileHash: 'test',
},
},
};
store.$patch(state);
store.scrollToFile({ path: 'path' });
expect(document.location.hash).toBe('#test');
});
it('commits SET_CURRENT_DIFF_FILE', () => {
const state = {
treeEntries: {
path: {
fileHash: 'test',
},
},
};
store.$patch(state);
store.scrollToFile({ path: 'path' });
expect(store[types.SET_CURRENT_DIFF_FILE]).toHaveBeenCalledWith('test');
});
});
describe('renderFileForDiscussionId', () => {
const notesState = {
discussions: [
{
id: '123',
diff_file: {
file_hash: 'HASH',
},
},
{
id: '456',
diff_file: {
file_hash: 'HASH',
},
},
],
};
let $emit;
const state = ({ collapsed, renderIt }) => ({
diffFiles: [
{
file_hash: 'HASH',
viewer: {
automaticallyCollapsed: collapsed,
},
renderIt,
},
],
});
beforeEach(() => {
useNotes().$patch(notesState);
$emit = jest.spyOn(eventHub, '$emit');
});
it('expands the file for the given discussion id', () => {
const localState = state({ collapsed: true, renderIt: false });
store.$patch(localState);
store.renderFileForDiscussionId('123');
expect($emit).toHaveBeenCalledTimes(1);
expect(commonUtils.scrollToElement).toHaveBeenCalledTimes(1);
});
it('jumps to discussion on already rendered and expanded file', () => {
const localState = state({ collapsed: false, renderIt: true });
store.$patch(localState);
store.renderFileForDiscussionId('123');
expect(store[types.SET_FILE_COLLAPSED]).not.toHaveBeenCalled();
expect($emit).toHaveBeenCalledTimes(1);
expect(commonUtils.scrollToElement).not.toHaveBeenCalled();
});
});
describe('setRenderTreeList', () => {
it('commits SET_RENDER_TREE_LIST', () => {
return testAction(
store.setRenderTreeList,
{ renderTreeList: true },
{},
[{ type: store[types.SET_RENDER_TREE_LIST], payload: true }],
[],
);
});
it('sets localStorage', () => {
store.setRenderTreeList({ renderTreeList: true });
expect(localStorage.setItem).toHaveBeenCalledWith('mr_diff_tree_list', true);
});
});
describe('setShowWhitespace', () => {
const endpointUpdateUser = 'user/prefs';
let putSpy;
beforeEach(() => {
jest.spyOn(api, 'trackRedisHllUserEvent').mockImplementation(() => {});
putSpy = jest.spyOn(axios, 'put');
mock.onPut(endpointUpdateUser).reply(HTTP_STATUS_OK, {});
jest.spyOn(eventHub, '$emit').mockImplementation();
});
it('commits SET_SHOW_WHITESPACE', () => {
return testAction(
store.setShowWhitespace,
{ showWhitespace: true, updateDatabase: false },
{},
[{ type: store[types.SET_SHOW_WHITESPACE], payload: true }],
[],
);
});
it('saves to the database when the user is logged in', async () => {
window.gon = { current_user_id: 12345 };
store.$patch({ endpointUpdateUser });
await store.setShowWhitespace({ showWhitespace: true, updateDatabase: true });
expect(putSpy).toHaveBeenCalledWith(endpointUpdateUser, { show_whitespace_in_diffs: true });
});
it('does not try to save to the API if the user is not logged in', async () => {
window.gon = {};
store.$patch({ endpointUpdateUser });
await store.setShowWhitespace({ showWhitespace: true, updateDatabase: true });
expect(putSpy).not.toHaveBeenCalled();
});
it('emits eventHub event', async () => {
await store.setShowWhitespace({ showWhitespace: true, updateDatabase: false });
expect(eventHub.$emit).toHaveBeenCalledWith('refetchDiffData');
});
});
describe('receiveFullDiffError', () => {
it('updates state with the file that did not load', () => {
return testAction(
store.receiveFullDiffError,
'file',
{ diffFiles: [{ file_path: 'file' }] },
[{ type: store[types.RECEIVE_FULL_DIFF_ERROR], payload: 'file' }],
[],
);
});
});
describe('fetchFullDiff', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/context`).replyOnce(HTTP_STATUS_OK, ['test']);
});
it('commits the success and dispatches an action to expand the new lines', () => {
const file = {
context_lines_path: `${TEST_HOST}/context`,
file_path: 'test',
file_hash: 'test',
};
return testAction(
store.fetchFullDiff,
file,
{ diffFiles: [file] },
[{ type: store[types.RECEIVE_FULL_DIFF_SUCCESS], payload: { filePath: 'test' } }],
[{ type: store.setExpandedDiffLines, payload: { file, data: ['test'] } }],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/context`).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches receiveFullDiffError', () => {
const file = {
context_lines_path: `${TEST_HOST}/context`,
file_path: 'test',
file_hash: 'test',
};
return testAction(
store.fetchFullDiff,
file,
{ diffFiles: [file] },
[],
[{ type: store.receiveFullDiffError, payload: 'test' }],
);
});
});
});
describe('toggleFullDiff', () => {
let state;
beforeEach(() => {
state = {
diffFiles: [{ file_path: 'test', isShowingFullFile: false }],
};
});
it('dispatches fetchFullDiff when file is not expanded', () => {
return testAction(
store.toggleFullDiff,
'test',
state,
[{ type: store[types.REQUEST_FULL_DIFF], payload: 'test' }],
[{ type: store.fetchFullDiff, payload: state.diffFiles[0] }],
);
});
});
describe('switchToFullDiffFromRenamedFile', () => {
const SUCCESS_URL = 'fakehost/context.success';
const testFilePath = 'testpath';
const updatedViewerName = 'testviewer';
const preparedLine = { prepared: 'in-a-test' };
const testFile = {
file_path: testFilePath,
file_hash: 'testhash',
alternate_viewer: { name: updatedViewerName },
};
const updatedViewer = {
name: updatedViewerName,
automaticallyCollapsed: false,
manuallyCollapsed: false,
forceOpen: false,
};
const testData = [{ rich_text: 'test' }, { rich_text: 'file2' }];
let renamedFile;
beforeEach(() => {
jest.spyOn(utils, 'prepareLineForRenamedFile').mockImplementation(() => preparedLine);
});
afterEach(() => {
renamedFile = null;
});
describe('success', () => {
beforeEach(() => {
renamedFile = { ...testFile, context_lines_path: SUCCESS_URL };
mock.onGet(SUCCESS_URL).replyOnce(HTTP_STATUS_OK, testData);
});
it.each`
diffViewType
${INLINE_DIFF_VIEW_TYPE}
${PARALLEL_DIFF_VIEW_TYPE}
`(
'performs the correct mutations and starts a render queue for view type $diffViewType',
({ diffViewType }) => {
return testAction(
store.switchToFullDiffFromRenamedFile,
{ diffFile: renamedFile },
{ diffFiles: [renamedFile], diffViewType },
[
{
type: store[types.SET_DIFF_FILE_VIEWER],
payload: { filePath: testFilePath, viewer: updatedViewer },
},
{
type: store[types.SET_CURRENT_VIEW_DIFF_FILE_LINES],
payload: { filePath: testFilePath, lines: [preparedLine, preparedLine] },
},
],
[],
);
},
);
});
});
describe('setFileCollapsedByUser', () => {
it('commits SET_FILE_COLLAPSED', () => {
return testAction(
store.setFileCollapsedByUser,
{ filePath: 'test', collapsed: true },
null,
[
{
type: store[types.SET_FILE_COLLAPSED],
payload: { filePath: 'test', collapsed: true, trigger: 'manual' },
},
],
[],
);
});
});
describe('setFileForcedOpen', () => {
it('commits SET_FILE_FORCED_OPEN', () => {
return testAction(
store.setFileForcedOpen,
{ filePath: 'test', forced: true },
{ diffFiles: [{ file_path: 'test', viewer: {} }] },
[
{
type: store[types.SET_FILE_FORCED_OPEN],
payload: { filePath: 'test', forced: true },
},
],
);
});
});
describe('setExpandedDiffLines', () => {
beforeEach(() => {
utils.idleCallback.mockImplementation((cb) => {
cb({ timeRemaining: () => 50 });
});
});
it('commits SET_CURRENT_VIEW_DIFF_FILE_LINES when lines less than MAX_RENDERING_DIFF_LINES', () => {
utils.convertExpandLines.mockImplementation(() => ['test']);
const file = { file_path: 'path' };
return testAction(
store.setExpandedDiffLines,
{ file, data: [] },
{ diffFiles: [file], diffViewType: 'inline' },
[
{
type: store.SET_CURRENT_VIEW_DIFF_FILE_LINES,
payload: { filePath: 'path', lines: ['test'] },
},
],
[],
);
});
it('commits ADD_CURRENT_VIEW_DIFF_FILE_LINES when lines more than MAX_RENDERING_DIFF_LINES', () => {
const lines = new Array(501).fill().map((_, i) => `line-${i}`);
utils.convertExpandLines.mockReturnValue(lines);
const file = { file_path: 'path' };
return testAction(
store.setExpandedDiffLines,
{ file, data: [] },
{ diffFiles: [file], diffViewType: 'inline' },
[
{
type: store.SET_CURRENT_VIEW_DIFF_FILE_LINES,
payload: { filePath: 'path', lines: lines.slice(0, 200) },
},
{ type: store.TOGGLE_DIFF_FILE_RENDERING_MORE, payload: 'path' },
...new Array(301).fill().map((_, i) => ({
type: store.ADD_CURRENT_VIEW_DIFF_FILE_LINES,
payload: { filePath: 'path', line: `line-${i + 200}` },
})),
{ type: store.TOGGLE_DIFF_FILE_RENDERING_MORE, payload: 'path' },
],
[],
);
});
});
describe('setSuggestPopoverDismissed', () => {
it('commits SET_SHOW_SUGGEST_POPOVER', async () => {
const state = { dismissEndpoint: `${TEST_HOST}/-/user_callouts` };
mock.onPost(state.dismissEndpoint).reply(HTTP_STATUS_OK, {});
jest.spyOn(axios, 'post');
await testAction(
store.setSuggestPopoverDismissed,
null,
state,
[{ type: store[types.SET_SHOW_SUGGEST_POPOVER] }],
[],
);
expect(axios.post).toHaveBeenCalledWith(state.dismissEndpoint, {
feature_name: 'suggest_popover_dismissed',
});
});
});
describe('changeCurrentCommit', () => {
it('commits the new commit information and re-requests the diff metadata for the commit', () => {
return testAction(
store.changeCurrentCommit,
{ commitId: 'NEW' },
{
commit: {
id: 'OLD',
},
endpoint: 'URL/OLD',
endpointBatch: 'URL/OLD',
endpointMetadata: 'URL/OLD',
},
[
{ type: store[types.SET_DIFF_FILES], payload: [] },
{
type: store[types.SET_BASE_CONFIG],
payload: {
...store.$state,
commit: {
id: 'OLD', // Not a typo: the action fired next will overwrite all of the `commit` in state
},
endpoint: 'URL/NEW',
endpointBatch: 'URL/NEW',
endpointMetadata: 'URL/NEW',
},
},
],
[{ type: store.fetchDiffFilesMeta }],
);
});
it.each`
commitId | commit | msg
${undefined} | ${{ id: 'OLD' }} | ${'`commitId` is a required argument'}
${'NEW'} | ${null} | ${'`state` must already contain a valid `commit`'}
${undefined} | ${null} | ${'`commitId` is a required argument'}
`(
'returns a rejected promise with the error message $msg given `{ "commitId": $commitId, "state.commit": $commit }`',
({ commitId, commit, msg }) => {
const err = new Error(msg);
const actionReturn = testAction(
store.changeCurrentCommit,
{ commitId },
{
endpoint: 'URL/OLD',
endpointBatch: 'URL/OLD',
endpointMetadata: 'URL/OLD',
commit,
},
[],
[],
);
return expect(actionReturn).rejects.toStrictEqual(err);
},
);
});
describe('moveToNeighboringCommit', () => {
it.each`
direction | expected | currentCommit
${'next'} | ${'NEXTSHA'} | ${{ next_commit_id: 'NEXTSHA' }}
${'previous'} | ${'PREVIOUSSHA'} | ${{ prev_commit_id: 'PREVIOUSSHA' }}
`(
'for the direction "$direction", dispatches the action to move to the SHA "$expected"',
({ direction, expected, currentCommit }) => {
return testAction(
store.moveToNeighboringCommit,
{ direction },
{ isLoading: false, commit: currentCommit },
[],
[{ type: store.changeCurrentCommit, payload: { commitId: expected } }],
);
},
);
it.each`
direction | diffsAreLoading | currentCommit
${'next'} | ${false} | ${{ prev_commit_id: 'PREVIOUSSHA' }}
${'next'} | ${true} | ${{ prev_commit_id: 'PREVIOUSSHA' }}
${'next'} | ${false} | ${undefined}
${'previous'} | ${false} | ${{ next_commit_id: 'NEXTSHA' }}
${'previous'} | ${true} | ${{ next_commit_id: 'NEXTSHA' }}
${'previous'} | ${false} | ${undefined}
`(
'given `{ "isloading": $diffsAreLoading, "commit": $currentCommit }` in state, no actions are dispatched',
({ direction, diffsAreLoading, currentCommit }) => {
return testAction(
store.moveToNeighboringCommit,
{ direction },
{ commit: currentCommit, isLoading: diffsAreLoading },
[],
[],
);
},
);
});
describe('rereadNoteHash', () => {
beforeEach(() => {
window.location.hash = 'note_123';
});
it('dispatches setCurrentDiffFileIdFromNote if the hash is a note URL', () => {
window.location.hash = 'note_123';
store.setCurrentDiffFileIdFromNote.mockReturnValueOnce(Promise.resolve());
return testAction(
store.rereadNoteHash,
{},
{},
[],
[{ type: store.setCurrentDiffFileIdFromNote, payload: '123' }],
);
});
it('dispatches fetchFileByFile if the app is in fileByFile mode', () => {
window.location.hash = 'note_123';
store.setCurrentDiffFileIdFromNote.mockReturnValueOnce(Promise.resolve());
return testAction(
store.rereadNoteHash,
{},
{ viewDiffsFileByFile: true },
[],
[
{ type: store.setCurrentDiffFileIdFromNote, payload: '123' },
{ type: store.fetchFileByFile },
],
);
});
it('does not try to fetch the diff file if the app is not in fileByFile mode', () => {
window.location.hash = 'note_123';
store.setCurrentDiffFileIdFromNote.mockReturnValueOnce(Promise.resolve());
return testAction(
store.rereadNoteHash,
{},
{ viewDiffsFileByFile: false },
[],
[{ type: store.setCurrentDiffFileIdFromNote, payload: '123' }],
);
});
it('does nothing if the hash is not a note URL', () => {
window.location.hash = 'abcdef1234567890';
return testAction(store.rereadNoteHash, {}, {}, [], []);
});
});
describe('setCurrentDiffFileIdFromNote', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
getters = { flatBlobsList: [{ fileHash: '123' }] };
notesGetters = {
getDiscussion: () => ({ diff_file: { file_hash: '123' } }),
notesById: { 1: { discussion_id: '2' } },
};
store.setCurrentDiffFileIdFromNote('1');
expect(store[types.SET_CURRENT_DIFF_FILE]).toHaveBeenCalledWith('123');
});
it('does not commit SET_CURRENT_DIFF_FILE when discussion has no diff_file', () => {
const commit = jest.fn();
notesGetters = {
getDiscussion: () => ({ id: '1' }),
notesById: { 1: { discussion_id: '2' } },
};
store.setCurrentDiffFileIdFromNote('1');
expect(commit).not.toHaveBeenCalled();
});
it('does not commit SET_CURRENT_DIFF_FILE when diff file does not exist', () => {
const commit = jest.fn();
getters = { flatBlobsList: [{ fileHash: '123' }] };
notesGetters = {
getDiscussion: () => ({ diff_file: { file_hash: '124' } }),
notesById: { 1: { discussion_id: '2' } },
};
store.setCurrentDiffFileIdFromNote('1');
expect(commit).not.toHaveBeenCalled();
});
});
describe('navigateToDiffFileIndex', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
getters = { flatBlobsList: [{ fileHash: '123' }] };
return testAction(
store.navigateToDiffFileIndex,
0,
null,
[{ type: store[types.SET_CURRENT_DIFF_FILE], payload: '123' }],
[{ type: store.unlinkFile }],
);
});
it('dispatches the fetchFileByFile action when the state value viewDiffsFileByFile is true', () => {
getters = { flatBlobsList: [{ fileHash: '123' }] };
store.fetchFileByFile.mockReturnValueOnce(Promise.resolve());
return testAction(
store.navigateToDiffFileIndex,
0,
{ viewDiffsFileByFile: true },
[{ type: store[types.SET_CURRENT_DIFF_FILE], payload: '123' }],
[{ type: store.unlinkFile }, { type: store.fetchFileByFile }],
);
});
});
describe('setFileByFile', () => {
const updateUserEndpoint = 'user/prefs';
let putSpy;
beforeEach(() => {
putSpy = jest.spyOn(axios, 'put');
mock.onPut(updateUserEndpoint).reply(HTTP_STATUS_OK, {});
});
it.each`
value
${true}
${false}
`(
'commits SET_FILE_BY_FILE and persists the File-by-File user preference with the new value $value',
async ({ value }) => {
await testAction(
store.setFileByFile,
{ fileByFile: value },
{
viewDiffsFileByFile: null,
endpointUpdateUser: updateUserEndpoint,
},
[{ type: store[types.SET_FILE_BY_FILE], payload: value }],
[],
);
expect(putSpy).toHaveBeenCalledWith(updateUserEndpoint, { view_diffs_file_by_file: value });
},
);
});
describe('reviewFile', () => {
const file = {
id: '123',
file_hash: 'xyz',
file_identifier_hash: 'abc',
load_collapsed_diff_url: 'gitlab-org/gitlab-test/-/merge_requests/1/diffs',
};
it.each`
reviews | diffFile | reviewed
${{ abc: ['123', 'hash:xyz'] }} | ${file} | ${true}
${{}} | ${file} | ${false}
`(
'sets reviews ($reviews) to localStorage and state for file $file if it is marked reviewed=$reviewed',
({ reviews, diffFile, reviewed }) => {
store.$patch({ mrReviews: { abc: ['123'] } });
store.reviewFile({
file: diffFile,
reviewed,
});
expect(localStorage.setItem).toHaveBeenCalledTimes(1);
expect(localStorage.setItem).toHaveBeenCalledWith(
'gitlab-org/gitlab-test/-/merge_requests/1-file-reviews',
JSON.stringify(reviews),
);
expect(store[types.SET_MR_FILE_REVIEWS]).toHaveBeenCalledWith(reviews);
},
);
});
describe('toggleFileCommentForm', () => {
it('commits TOGGLE_FILE_COMMENT_FORM', () => {
const file = getDiffFileMock();
return testAction(
store.toggleFileCommentForm,
file.file_path,
{
diffFiles: [file],
},
[
{ type: store[types.TOGGLE_FILE_COMMENT_FORM], payload: file.file_path },
{
type: store[types.SET_FILE_COLLAPSED],
payload: { filePath: file.file_path, collapsed: false },
},
],
[],
);
});
it('always opens if file is collapsed', () => {
const file = {
...getDiffFileMock(),
viewer: {
...getDiffFileMock().viewer,
manuallyCollapsed: true,
},
};
return testAction(
store.toggleFileCommentForm,
file.file_path,
{
diffFiles: [file],
},
[
{
type: store[types.SET_FILE_COMMENT_FORM],
payload: { filePath: file.file_path, expanded: true },
},
{
type: store[types.SET_FILE_COLLAPSED],
payload: { filePath: file.file_path, collapsed: false },
},
],
[],
);
});
});
describe('addDraftToFile', () => {
it('commits ADD_DRAFT_TO_FILE', () => {
return testAction(
store.addDraftToFile,
{ filePath: 'path', draft: 'draft' },
{},
[{ type: store[types.ADD_DRAFT_TO_FILE], payload: { filePath: 'path', draft: 'draft' } }],
[],
);
});
});
describe('fetchLinkedFile', () => {
it('fetches linked file', async () => {
const linkedFileHref = `${TEST_HOST}/linked-file`;
const linkedFile = getDiffFileMock();
const diffFiles = [linkedFile];
const hubSpy = jest.spyOn(diffsEventHub, '$emit');
mock.onGet(new RegExp(linkedFileHref)).reply(HTTP_STATUS_OK, { diff_files: diffFiles });
await testAction(
store.fetchLinkedFile,
linkedFileHref,
{},
[
{ type: store[types.SET_BATCH_LOADING_STATE], payload: 'loading' },
{ type: store[types.SET_RETRIEVING_BATCHES], payload: true },
{
type: store[types.SET_DIFF_DATA_BATCH],
payload: {
get diff_files() {
return store.diffFiles;
},
updatePosition: false,
},
},
{ type: store[types.SET_LINKED_FILE_HASH], payload: linkedFile.file_hash },
{ type: store[types.SET_CURRENT_DIFF_FILE], payload: linkedFile.file_hash },
{ type: store[types.SET_BATCH_LOADING_STATE], payload: 'loaded' },
{ type: store[types.SET_RETRIEVING_BATCHES], payload: false },
],
[],
);
jest.runAllTimers();
expect(hubSpy).toHaveBeenCalledWith('diffFilesModified');
expect(handleLocationHash).toHaveBeenCalled();
});
it('handles load error', async () => {
const linkedFileHref = `${TEST_HOST}/linked-file`;
const hubSpy = jest.spyOn(diffsEventHub, '$emit');
mock.onGet(new RegExp(linkedFileHref)).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
try {
await testAction(
store.fetchLinkedFile,
linkedFileHref,
{},
[
{ type: store[types.SET_BATCH_LOADING_STATE], payload: 'loading' },
{ type: store[types.SET_RETRIEVING_BATCHES], payload: true },
{ type: store[types.SET_BATCH_LOADING_STATE], payload: 'error' },
{ type: store[types.SET_RETRIEVING_BATCHES], payload: false },
],
[],
);
} catch (error) {
expect(error.response.status).toBe(HTTP_STATUS_INTERNAL_SERVER_ERROR);
}
jest.runAllTimers();
expect(hubSpy).not.toHaveBeenCalledWith('diffFilesModified');
expect(handleLocationHash).not.toHaveBeenCalled();
});
it('fetches linked context lines', async () => {
const linkedFile = getDiffFileMock();
const diffFiles = [linkedFile];
window.location.hash = `#${linkedFile.file_hash}_10_10`;
const linkedFileHref = `${TEST_HOST}/linked-file`;
jest.spyOn(diffsEventHub, '$emit');
mock.onGet(new RegExp(linkedFileHref)).reply(HTTP_STATUS_OK, { diff_files: diffFiles });
store.fetchLinkedExpandedLine.mockResolvedValue();
await store.fetchLinkedFile(linkedFileHref);
expect(store.fetchLinkedExpandedLine).toHaveBeenCalledWith({
fileHash: linkedFile.file_hash,
oldLine: 10,
newLine: 10,
});
});
});
describe('unlinkFile', () => {
it('unlinks linked file', () => {
const linkedFile = getDiffFileMock();
setWindowLocation(`${TEST_HOST}/?file=${linkedFile.file_hash}#${linkedFile.file_hash}_10_10`);
testAction(
store.unlinkFile,
undefined,
{ diffFiles: [linkedFile], linkedFileHash: linkedFile.file_hash },
[{ type: store[types.SET_LINKED_FILE_HASH], payload: null }],
[],
);
expect(window.location.hash).toBe('');
expect(window.location.search).toBe('');
});
it('does nothing when no linked file present', () => {
testAction(store.unlinkFile, undefined, {}, [], []);
});
});
describe('expandAllFiles', () => {
it('triggers mutation', () => {
testAction(
store.expandAllFiles,
undefined,
{},
[
{
type: store[types.SET_COLLAPSED_STATE_FOR_ALL_FILES],
payload: { collapsed: false },
},
],
[],
);
});
});
describe('collapseAllFiles', () => {
it('triggers mutation', () => {
testAction(
store.collapseAllFiles,
undefined,
{},
[
{
type: store[types.SET_COLLAPSED_STATE_FOR_ALL_FILES],
payload: { collapsed: true },
},
],
[],
);
});
});
describe('fetchLinkedExpandedLine', () => {
it('does nothing when no linked file is present', () => {
return testAction(
store.fetchLinkedExpandedLine,
{ fileHash: 'foo', oldLine: 10, newLine: 10 },
{ linkedFileHash: null },
[],
[],
);
});
it("does nothing when fragment doesn't match linked file hash", () => {
return testAction(
store.fetchLinkedExpandedLine,
{ fileHash: 'foo', oldLine: 10, newLine: 10 },
{ linkedFileHash: 'foobar' },
[],
[],
);
});
it('does nothing when line is already present', () => {
return testAction(
store.fetchLinkedExpandedLine,
{ fileHash: 'foo', oldLine: 10, newLine: 10 },
{
linkedFileHash: 'foo',
diffFiles: [
{ file_hash: 'foo', highlighted_diff_lines: [{ old_line: 10, new_line: 10 }] },
],
},
[],
[],
);
});
it('propagates the error when fetching expanded line data', () => {
const fakeError = new Error();
store.loadMoreLines.mockRejectedValueOnce(fakeError);
const linkedFile = {
file_hash: 'abc123',
highlighted_diff_lines: [
{ new_line: 1, old_line: 1 },
{ meta_data: { old_pos: 2, new_pos: 2 } },
],
};
store.$patch({
diffFiles: [linkedFile],
linkedFileHash: linkedFile.file_hash,
});
return store
.fetchLinkedExpandedLine({ fileHash: linkedFile.file_hash, oldLine: 10, newLine: 10 })
.catch((error) => {
expect(error).toBe(fakeError);
});
});
describe('expansion', () => {
beforeEach(() => {
store.loadMoreLines.mockResolvedValue();
});
it('expands lines at the very top', async () => {
const linkedFile = {
file_hash: 'abc123',
context_lines_path: `${TEST_HOST}/linked-file`,
highlighted_diff_lines: [
{ meta_data: { old_pos: 5, new_pos: 6 } },
{ old_line: 5, new_line: 6 },
],
};
store.$patch({
diffFiles: [linkedFile],
linkedFileHash: linkedFile.file_hash,
});
await store.fetchLinkedExpandedLine({
fileHash: linkedFile.file_hash,
oldLine: 1,
newLine: 1,
});
expect(store.loadMoreLines).toHaveBeenCalledWith({
endpoint: linkedFile.context_lines_path,
fileHash: linkedFile.file_hash,
isExpandDown: false,
lineNumbers: {
oldLineNumber: 5,
newLineNumber: 6,
},
params: {
bottom: false,
offset: 1,
since: 1,
to: 5,
unfold: true,
},
});
});
it('expands lines upwards in the middle of the file', async () => {
const linkedFile = {
file_hash: 'abc123',
context_lines_path: `${TEST_HOST}/linked-file`,
highlighted_diff_lines: [
{ old_line: 5, new_line: 6 },
{ meta_data: { old_pos: 50, new_pos: 51 } },
{ old_line: 50, new_line: 51 },
],
};
store.$patch({
diffFiles: [linkedFile],
linkedFileHash: linkedFile.file_hash,
});
await store.fetchLinkedExpandedLine({
fileHash: linkedFile.file_hash,
oldLine: 45,
newLine: 45,
});
expect(store.loadMoreLines).toHaveBeenCalledWith({
endpoint: linkedFile.context_lines_path,
fileHash: linkedFile.file_hash,
isExpandDown: false,
lineNumbers: {
oldLineNumber: 50,
newLineNumber: 51,
},
params: {
bottom: false,
offset: 1,
since: 45,
to: 50,
unfold: true,
},
});
});
it('expands lines in both directions', async () => {
const linkedFile = {
file_hash: 'abc123',
context_lines_path: `${TEST_HOST}/linked-file`,
highlighted_diff_lines: [
{ old_line: 5, new_line: 6 },
{ meta_data: { old_pos: 10, new_pos: 11 } },
{ new_line: 10, old_line: 11 },
],
};
store.$patch({
diffFiles: [linkedFile],
linkedFileHash: linkedFile.file_hash,
});
await store.fetchLinkedExpandedLine({
fileHash: linkedFile.file_hash,
oldLine: 7,
newLine: 8,
});
expect(store.loadMoreLines).toHaveBeenCalledWith({
endpoint: linkedFile.context_lines_path,
fileHash: linkedFile.file_hash,
isExpandDown: false,
lineNumbers: {
oldLineNumber: 10,
newLineNumber: 11,
},
params: {
bottom: false,
offset: 1,
since: 7,
to: 10,
unfold: false,
},
});
});
it('expands lines downwards in the middle of the file', async () => {
const linkedFile = {
file_hash: 'abc123',
context_lines_path: `${TEST_HOST}/linked-file`,
highlighted_diff_lines: [
{ old_line: 5, new_line: 6 },
{ meta_data: { old_pos: 50, new_pos: 51 } },
{ old_line: 50, new_line: 51 },
],
};
store.$patch({
diffFiles: [linkedFile],
linkedFileHash: linkedFile.file_hash,
});
await store.fetchLinkedExpandedLine({
fileHash: linkedFile.file_hash,
oldLine: 7,
newLine: 8,
});
expect(store.loadMoreLines).toHaveBeenCalledWith({
endpoint: linkedFile.context_lines_path,
fileHash: linkedFile.file_hash,
isExpandDown: true,
lineNumbers: {
oldLineNumber: 5,
newLineNumber: 6,
},
nextLineNumbers: {
old_line: 50,
new_line: 51,
},
params: {
bottom: true,
offset: 1,
since: 7,
to: 8,
unfold: true,
},
});
});
it('expands lines at the very bottom', async () => {
const linkedFile = {
file_hash: 'abc123',
context_lines_path: `${TEST_HOST}/linked-file`,
highlighted_diff_lines: [
{ old_line: 5, new_line: 6 },
{ meta_data: { old_pos: 5, new_pos: 6 } },
],
};
store.$patch({
diffFiles: [linkedFile],
linkedFileHash: linkedFile.file_hash,
});
await store.fetchLinkedExpandedLine({
fileHash: linkedFile.file_hash,
oldLine: 20,
newLine: 21,
});
expect(store.loadMoreLines).toHaveBeenCalledWith({
endpoint: linkedFile.context_lines_path,
fileHash: linkedFile.file_hash,
isExpandDown: false,
lineNumbers: {
oldLineNumber: 5,
newLineNumber: 6,
},
params: {
bottom: true,
offset: 1,
since: 7,
to: 21,
unfold: true,
},
});
});
});
});
});