diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-11 21:09:22 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-11 21:09:22 +0300 |
commit | 1bd9d2d9499d0d28e62254a28fcd3d913a8704af (patch) | |
tree | ea9969a5a4c3ac77858be20d69869674bed5ca43 /spec/frontend/diffs | |
parent | d8877c12347443fa02e0ba53ad8d5cd318f6fa28 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/diffs')
-rw-r--r-- | spec/frontend/diffs/components/__snapshots__/tree_list_spec.js.snap | 160 | ||||
-rw-r--r-- | spec/frontend/diffs/components/app_spec.js | 39 | ||||
-rw-r--r-- | spec/frontend/diffs/components/diff_file_header_spec.js | 27 | ||||
-rw-r--r-- | spec/frontend/diffs/components/diff_file_spec.js | 146 | ||||
-rw-r--r-- | spec/frontend/diffs/components/diff_row_utils_spec.js | 39 | ||||
-rw-r--r-- | spec/frontend/diffs/components/tree_list_spec.js | 103 | ||||
-rw-r--r-- | spec/frontend/diffs/store/actions_spec.js | 124 | ||||
-rw-r--r-- | spec/frontend/diffs/store/getters_spec.js | 32 | ||||
-rw-r--r-- | spec/frontend/diffs/store/mutations_spec.js | 57 | ||||
-rw-r--r-- | spec/frontend/diffs/store/utils_spec.js | 11 |
10 files changed, 626 insertions, 112 deletions
diff --git a/spec/frontend/diffs/components/__snapshots__/tree_list_spec.js.snap b/spec/frontend/diffs/components/__snapshots__/tree_list_spec.js.snap new file mode 100644 index 00000000000..605f6335b5c --- /dev/null +++ b/spec/frontend/diffs/components/__snapshots__/tree_list_spec.js.snap @@ -0,0 +1,160 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Diffs tree list component pinned file files in folders pins 1.rb file 1`] = ` +Array [ + "📁folder/", + "──1.rb", + "📁folder", + "──📁sub-folder", + "────nested-1.rb", + "────nested-2.rb", + "────nested-3.rb", + "──2.rb", + "──3.rb", + "📁folder-single", + "──single.rb", + "root-first.rb", + "root-last.rb", +] +`; + +exports[`Diffs tree list component pinned file files in folders pins 2.rb file 1`] = ` +Array [ + "📁folder/", + "──2.rb", + "📁folder", + "──📁sub-folder", + "────nested-1.rb", + "────nested-2.rb", + "────nested-3.rb", + "──1.rb", + "──3.rb", + "📁folder-single", + "──single.rb", + "root-first.rb", + "root-last.rb", +] +`; + +exports[`Diffs tree list component pinned file files in folders pins 3.rb file 1`] = ` +Array [ + "📁folder/", + "──3.rb", + "📁folder", + "──📁sub-folder", + "────nested-1.rb", + "────nested-2.rb", + "────nested-3.rb", + "──1.rb", + "──2.rb", + "📁folder-single", + "──single.rb", + "root-first.rb", + "root-last.rb", +] +`; + +exports[`Diffs tree list component pinned file files in folders pins nested-1.rb file 1`] = ` +Array [ + "📁folder/sub-folder/", + "──nested-1.rb", + "📁folder", + "──📁sub-folder", + "────nested-2.rb", + "────nested-3.rb", + "──1.rb", + "──2.rb", + "──3.rb", + "📁folder-single", + "──single.rb", + "root-first.rb", + "root-last.rb", +] +`; + +exports[`Diffs tree list component pinned file files in folders pins nested-2.rb file 1`] = ` +Array [ + "📁folder/sub-folder/", + "──nested-2.rb", + "📁folder", + "──📁sub-folder", + "────nested-1.rb", + "────nested-3.rb", + "──1.rb", + "──2.rb", + "──3.rb", + "📁folder-single", + "──single.rb", + "root-first.rb", + "root-last.rb", +] +`; + +exports[`Diffs tree list component pinned file files in folders pins nested-3.rb file 1`] = ` +Array [ + "📁folder/sub-folder/", + "──nested-3.rb", + "📁folder", + "──📁sub-folder", + "────nested-1.rb", + "────nested-2.rb", + "──1.rb", + "──2.rb", + "──3.rb", + "📁folder-single", + "──single.rb", + "root-first.rb", + "root-last.rb", +] +`; + +exports[`Diffs tree list component pinned file files in folders pins root-first.rb file 1`] = ` +Array [ + "root-first.rb", + "📁folder", + "──📁sub-folder", + "────nested-1.rb", + "────nested-2.rb", + "────nested-3.rb", + "──1.rb", + "──2.rb", + "──3.rb", + "📁folder-single", + "──single.rb", + "root-last.rb", +] +`; + +exports[`Diffs tree list component pinned file files in folders pins root-last.rb file 1`] = ` +Array [ + "root-last.rb", + "📁folder", + "──📁sub-folder", + "────nested-1.rb", + "────nested-2.rb", + "────nested-3.rb", + "──1.rb", + "──2.rb", + "──3.rb", + "📁folder-single", + "──single.rb", + "root-first.rb", +] +`; + +exports[`Diffs tree list component pinned file files in folders pins single.rb file 1`] = ` +Array [ + "📁folder-single/", + "──single.rb", + "📁folder", + "──📁sub-folder", + "────nested-1.rb", + "────nested-2.rb", + "────nested-3.rb", + "──1.rb", + "──2.rb", + "──3.rb", + "root-first.rb", + "root-last.rb", +] +`; diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js index 63d9a2471b6..813db12e83f 100644 --- a/spec/frontend/diffs/components/app_spec.js +++ b/spec/frontend/diffs/components/app_spec.js @@ -31,6 +31,9 @@ import * as urlUtils from '~/lib/utils/url_utility'; import * as commonUtils from '~/lib/utils/common_utils'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { stubPerformanceWebAPI } from 'helpers/performance'; +import { getDiffFileMock } from 'jest/diffs/mock_data/diff_file'; +import waitForPromises from 'helpers/wait_for_promises'; +import { diffMetadata } from 'jest/diffs/mock_data/diff_metadata'; import createDiffsStore from '../create_diffs_store'; import diffsMockData from '../mock_data/merge_request_diffs'; @@ -38,6 +41,8 @@ const mergeRequestDiff = { version_index: 1 }; const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`; const COMMIT_URL = `${TEST_HOST}/COMMIT/OLD`; const UPDATED_COMMIT_URL = `${TEST_HOST}/COMMIT/NEW`; +const ENDPOINT_BATCH_URL = `${TEST_HOST}/diff/endpointBatch`; +const ENDPOINT_METADATA_URL = `${TEST_HOST}/diff/endpointMetadata`; Vue.use(Vuex); Vue.use(VueApollo); @@ -77,8 +82,8 @@ describe('diffs/components/app', () => { store.dispatch('diffs/setBaseConfig', { endpoint: TEST_ENDPOINT, - endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`, - endpointBatch: `${TEST_HOST}/diff/endpointBatch`, + endpointMetadata: ENDPOINT_METADATA_URL, + endpointBatch: ENDPOINT_BATCH_URL, endpointDiffForPath: TEST_ENDPOINT, projectPath: 'namespace/project', dismissEndpoint: '', @@ -126,7 +131,7 @@ describe('diffs/components/app', () => { const fetchResolver = () => { store.state.diffs.retrievingBatches = false; store.state.notes.doneFetchingBatchDiscussions = true; - store.state.notes.discussions = 'test'; + store.state.notes.discussions = []; return Promise.resolve({ real_size: 100 }); }; jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn()); @@ -861,4 +866,32 @@ describe('diffs/components/app', () => { expect(loadSpy).not.toHaveBeenCalledWith({ file: store.state.diffs.diffFiles[0] }); }); }); + + describe('pinned file', () => { + const pinnedFileUrl = 'http://localhost.test/pinned-file'; + let pinnedFile; + + beforeEach(() => { + pinnedFile = getDiffFileMock(); + mock.onGet(pinnedFileUrl).reply(HTTP_STATUS_OK, { diff_files: [pinnedFile] }); + mock + .onGet(new RegExp(ENDPOINT_BATCH_URL)) + .reply(HTTP_STATUS_OK, { diff_files: [], pagination: {} }); + mock.onGet(new RegExp(ENDPOINT_METADATA_URL)).reply(HTTP_STATUS_OK, diffMetadata); + + createComponent({ shouldShow: true, pinnedFileUrl }); + }); + + it('fetches and displays pinned file', async () => { + await waitForPromises(); + + expect(wrapper.findComponent({ name: 'DynamicScroller' }).props('items')[0].file_hash).toBe( + pinnedFile.file_hash, + ); + }); + + it('shows a spinner during loading', () => { + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js index d6539a5bffa..c02875963fd 100644 --- a/spec/frontend/diffs/components/diff_file_header_spec.js +++ b/spec/frontend/diffs/components/diff_file_header_spec.js @@ -1,8 +1,8 @@ -import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import { cloneDeep } from 'lodash'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mockTracking, triggerEvent } from 'helpers/tracking_helper'; @@ -20,6 +20,7 @@ import { truncateSha } from '~/lib/utils/text_utility'; import { __, sprintf } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { TEST_HOST } from 'spec/test_constants'; import testAction from '../../__helpers__/vuex_action_helper'; import diffDiscussionsMockData from '../mock_data/diff_discussions'; @@ -73,6 +74,7 @@ describe('DiffFileHeader component', () => { setFileCollapsedByUser: jest.fn(), setFileForcedOpen: jest.fn(), reviewFile: jest.fn(), + unpinFile: jest.fn(), }, }, }, @@ -87,7 +89,7 @@ describe('DiffFileHeader component', () => { }); const findHeader = () => wrapper.findComponent({ ref: 'header' }); - const findTitleLink = () => wrapper.findComponent({ ref: 'titleWrapper' }); + const findTitleLink = () => wrapper.findByTestId('file-title'); const findExpandButton = () => wrapper.findComponent({ ref: 'expandDiffToFullFileButton' }); const findFileActions = () => wrapper.find('.file-actions'); const findModeChangedLine = () => wrapper.findComponent({ ref: 'fileMode' }); @@ -105,7 +107,7 @@ describe('DiffFileHeader component', () => { mockStoreConfig = cloneDeep(defaultMockStoreConfig); const store = new Vuex.Store({ ...mockStoreConfig, ...options.store }); - wrapper = shallowMount(DiffFileHeader, { + wrapper = shallowMountExtended(DiffFileHeader, { propsData: { diffFile, canCurrentUserFork: false, @@ -711,4 +713,23 @@ describe('DiffFileHeader component', () => { expect(wrapper.find('[data-testid="comment-files-button"]').exists()).toEqual(true); }); + + describe('pinned file', () => { + beforeEach(() => { + window.gon.features = { pinnedFile: true }; + }); + + it('has pinned URL search param', () => { + createComponent(); + const url = new URL(TEST_HOST + findTitleLink().attributes('href')); + expect(url.searchParams.get('pin')).toBe(diffFile.file_hash); + }); + + it('can unpin file', () => { + createComponent({ props: { addMergeRequestButtons: true, pinned: true } }); + const unpinButton = wrapper.findComponentByTestId('unpin-button'); + unpinButton.vm.$emit('click'); + expect(mockStoreConfig.modules.diffs.actions.unpinFile).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js index a9fbf4632ac..444f4102e26 100644 --- a/spec/frontend/diffs/components/diff_file_spec.js +++ b/spec/frontend/diffs/components/diff_file_spec.js @@ -29,6 +29,7 @@ import createNotesStore from '~/notes/stores/modules'; import diffsModule from '~/diffs/store/modules'; import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n'; import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; +import { SET_PINNED_FILE_HASH } from '~/diffs/store/mutation_types'; import { getDiffFileMock } from '../mock_data/diff_file'; import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable'; import diffsMockData from '../mock_data/merge_request_diffs'; @@ -90,49 +91,6 @@ function markFileToBeRendered(store, index = 0) { }); } -function createComponent({ file, first = false, last = false, options = {}, props = {} }) { - const diffs = diffsModule(); - diffs.actions = { - ...diffs.actions, - prefetchFileNeighbors: prefetchFileNeighborsMock, - saveDiffDiscussion: saveDiffDiscussionMock, - }; - - diffs.getters = { - ...diffs.getters, - diffCompareDropdownTargetVersions: () => [], - diffCompareDropdownSourceVersions: () => [], - }; - - const store = new Vuex.Store({ - ...createNotesStore(), - modules: { diffs }, - }); - - store.state.diffs = { - mergeRequestDiff: diffsMockData[0], - diffFiles: [file], - }; - - const wrapper = shallowMountExtended(DiffFileComponent, { - store, - propsData: { - file, - canCurrentUserFork: false, - viewDiffsFileByFile: false, - isFirstFile: first, - isLastFile: last, - ...props, - }, - ...options, - }); - - return { - wrapper, - store, - }; -} - const findDiffHeader = (wrapper) => wrapper.findComponent(DiffFileHeaderComponent); const findDiffContentArea = (wrapper) => wrapper.findByTestId('content-area'); const findLoader = (wrapper) => wrapper.findByTestId('loader-icon'); @@ -159,15 +117,58 @@ const triggerSaveDraftNote = (wrapper, note, parent, error) => findNoteForm(wrapper).vm.$emit('handleFormUpdateAddToReview', note, false, parent, error); describe('DiffFile', () => { - let readableFile; let wrapper; let store; let axiosMock; + function createComponent({ + file = getReadableFile(), + first = false, + last = false, + options = {}, + props = {}, + } = {}) { + const diffs = diffsModule(); + diffs.actions = { + ...diffs.actions, + prefetchFileNeighbors: prefetchFileNeighborsMock, + saveDiffDiscussion: saveDiffDiscussionMock, + }; + + diffs.getters = { + ...diffs.getters, + diffCompareDropdownTargetVersions: () => [], + diffCompareDropdownSourceVersions: () => [], + }; + + store = new Vuex.Store({ + ...createNotesStore(), + modules: { diffs }, + }); + + store.state.diffs = { + ...store.state.diffs, + mergeRequestDiff: diffsMockData[0], + diffFiles: [file], + }; + + wrapper = shallowMountExtended(DiffFileComponent, { + store, + propsData: { + file, + canCurrentUserFork: false, + viewDiffsFileByFile: false, + isFirstFile: first, + isLastFile: last, + ...props, + }, + ...options, + }); + } + beforeEach(() => { - readableFile = getReadableFile(); axiosMock = new MockAdapter(axios); - ({ wrapper, store } = createComponent({ file: readableFile })); + createComponent(); }); afterEach(() => { @@ -186,7 +187,6 @@ describe('DiffFile', () => { `('$description', ({ fileByFile }) => { createComponent({ props: { viewDiffsFileByFile: fileByFile }, - file: readableFile, }); if (fileByFile) { @@ -217,11 +217,11 @@ describe('DiffFile', () => { forceHasDiff({ store, ...file }); } - ({ wrapper, store } = createComponent({ + createComponent({ file: store.state.diffs.diffFiles[0], first, last, - })); + }); await nextTick(); @@ -233,14 +233,13 @@ describe('DiffFile', () => { ); it('emits the "first file shown" and "files end" events when in File-by-File mode', async () => { - ({ wrapper, store } = createComponent({ - file: getReadableFile(), + createComponent({ first: false, last: false, props: { viewDiffsFileByFile: true, }, - })); + }); await nextTick(); @@ -253,11 +252,11 @@ describe('DiffFile', () => { describe('after loading the diff', () => { it('indicates that it loaded the file', async () => { forceHasDiff({ store, inlineLines: [], parallelLines: [], readableText: true }); - ({ wrapper, store } = createComponent({ + createComponent({ file: store.state.diffs.diffFiles[0], first: true, last: true, - })); + }); jest.spyOn(wrapper.vm, 'loadCollapsedDiff').mockResolvedValue(getReadableFile()); jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn()); @@ -314,11 +313,11 @@ describe('DiffFile', () => { `('should be $bool when { userIsLoggedIn: $loggedIn }', ({ loggedIn, bool }) => { setLoggedIn(loggedIn); - ({ wrapper } = createComponent({ + createComponent({ props: { file: store.state.diffs.diffFiles[0], }, - })); + }); expect(wrapper.vm.showLocalFileReviews).toBe(bool); }); @@ -556,7 +555,7 @@ describe('DiffFile', () => { describe('general (other) collapsed', () => { it('should be expandable for unreadable files', async () => { - ({ wrapper, store } = createComponent({ file: getUnreadableFile() })); + createComponent({ file: getUnreadableFile() }); makeFileAutomaticallyCollapsed(store); await nextTick(); @@ -622,7 +621,7 @@ describe('DiffFile', () => { renderIt: true, }; - ({ wrapper, store } = createComponent({ file })); + createComponent({ file }); expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(false); }); @@ -634,7 +633,7 @@ describe('DiffFile', () => { renderIt: true, }; - ({ wrapper, store } = createComponent({ file })); + createComponent({ file }); expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(true); }); @@ -656,9 +655,9 @@ describe('DiffFile', () => { ...extraProps, }; - ({ wrapper, store } = createComponent({ + createComponent({ file, - })); + }); expect(wrapper.findByTestId('file-discussions').exists()).toEqual(exists); }, @@ -676,9 +675,9 @@ describe('DiffFile', () => { hasCommentForm, }; - ({ wrapper, store } = createComponent({ + createComponent({ file, - })); + }); expect(findNoteForm(wrapper).exists()).toEqual(exists); }, @@ -694,9 +693,9 @@ describe('DiffFile', () => { discussions, }; - ({ wrapper, store } = createComponent({ + createComponent({ file, - })); + }); expect(wrapper.findByTestId('diff-file-discussions').exists()).toEqual(exists); }); @@ -712,10 +711,10 @@ describe('DiffFile', () => { const errorCallback = jest.fn(); beforeEach(() => { - ({ wrapper, store } = createComponent({ + createComponent({ file, options: { provide: { glFeatures: { commentOnFiles: true } } }, - })); + }); }); it('calls saveDiffDiscussionMock', () => { @@ -771,10 +770,10 @@ describe('DiffFile', () => { const errorCallback = jest.fn(); beforeEach(async () => { - ({ wrapper, store } = createComponent({ + createComponent({ file, options: { provide: { glFeatures: { commentOnFiles: true } } }, - })); + }); triggerSaveDraftNote(wrapper, note, parentElement, errorCallback); @@ -791,4 +790,13 @@ describe('DiffFile', () => { }); }); }); + + describe('pinned file', () => { + it('passes down pinned prop', async () => { + createComponent(); + store.commit(`diffs/${SET_PINNED_FILE_HASH}`, getReadableFile().file_hash); + await nextTick(); + expect(wrapper.findComponent(DiffFileHeaderComponent).props('pinned')).toBe(true); + }); + }); }); diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js index 6e9eb433924..bd9592e4f5e 100644 --- a/spec/frontend/diffs/components/diff_row_utils_spec.js +++ b/spec/frontend/diffs/components/diff_row_utils_spec.js @@ -6,6 +6,7 @@ import { NEW_NO_NEW_LINE_TYPE, EMPTY_CELL_TYPE, } from '~/diffs/constants'; +import { getDiffFileMock } from 'jest/diffs/mock_data/diff_file'; const LINE_CODE = 'abc123'; @@ -108,15 +109,47 @@ describe('diff_row_utils', () => { describe('lineHref', () => { it(`should return #${LINE_CODE}`, () => { - expect(utils.lineHref({ line_code: LINE_CODE })).toEqual(`#${LINE_CODE}`); + expect(utils.lineHref({ line_code: LINE_CODE }, {})).toEqual(`#${LINE_CODE}`); }); it(`should return '#' if line is undefined`, () => { - expect(utils.lineHref()).toEqual('#'); + expect(utils.lineHref()).toEqual(''); }); it(`should return '#' if line_code is undefined`, () => { - expect(utils.lineHref({})).toEqual('#'); + expect(utils.lineHref({}, {})).toEqual(''); + }); + + describe('pinned file', () => { + beforeEach(() => { + window.gon.features = { pinnedFile: true }; + }); + + afterEach(() => { + delete window.gon.features; + }); + + it(`should return pinned file URL`, () => { + const diffFile = getDiffFileMock(); + expect(utils.lineHref({ line_code: LINE_CODE }, { diffFile })).toEqual( + `?pin=${diffFile.file_hash}#${LINE_CODE}`, + ); + }); + }); + }); + + describe('pinnedFileHref', () => { + beforeEach(() => { + window.gon.features = { pinnedFile: true }; + }); + + afterEach(() => { + delete window.gon.features; + }); + + it(`should return pinned file URL`, () => { + const diffFile = getDiffFileMock(); + expect(utils.pinnedFileHref(diffFile)).toEqual(`?pin=${diffFile.file_hash}`); }); }); diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js index a54cf9b8bff..230839f0ecf 100644 --- a/spec/frontend/diffs/components/tree_list_spec.js +++ b/spec/frontend/diffs/components/tree_list_spec.js @@ -7,6 +7,9 @@ import batchComments from '~/batch_comments/stores/modules/batch_comments'; import DiffFileRow from '~/diffs/components//diff_file_row.vue'; import { stubComponent } from 'helpers/stub_component'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { SET_PINNED_FILE_HASH, SET_TREE_DATA, SET_DIFF_FILES } from '~/diffs/store/mutation_types'; +import { generateTreeList } from '~/diffs/utils/tree_worker_utils'; +import { sortTree } from '~/ide/stores/utils'; describe('Diffs tree list component', () => { let wrapper; @@ -58,6 +61,14 @@ describe('Diffs tree list component', () => { const setupFilesInState = () => { const treeEntries = { + app: { + key: 'app', + path: 'app', + name: 'app', + type: 'tree', + tree: [], + opened: true, + }, 'index.js': { addedLines: 0, changed: true, @@ -71,6 +82,8 @@ describe('Diffs tree list component', () => { type: 'blob', parentPath: 'app', tree: [], + file_path: 'app/index.js', + file_hash: 'app-index', }, 'test.rb': { addedLines: 0, @@ -85,20 +98,39 @@ describe('Diffs tree list component', () => { type: 'blob', parentPath: 'app', tree: [], + file_path: 'app/test.rb', + file_hash: 'app-test', }, - app: { - key: 'app', - path: 'app', - name: 'app', - type: 'tree', + LICENSE: { + addedLines: 0, + changed: true, + deleted: false, + fileHash: 'LICENSE', + key: 'LICENSE', + name: 'LICENSE', + path: 'LICENSE', + removedLines: 0, + tempFile: true, + type: 'blob', + parentPath: '/', tree: [], + file_path: 'LICENSE', + file_hash: 'LICENSE', }, }; Object.assign(store.state.diffs, { treeEntries, - tree: [treeEntries['index.js'], treeEntries.app], + tree: [ + treeEntries.LICENSE, + { + ...treeEntries.app, + tree: [treeEntries['index.js'], treeEntries['test.rb']], + }, + ], }); + + return treeEntries; }; describe('default', () => { @@ -149,7 +181,7 @@ describe('Diffs tree list component', () => { }); it('renders tree', () => { - expect(getScroller().props('items')).toHaveLength(2); + expect(getScroller().props('items')).toHaveLength(4); }); it('hides file stats', () => { @@ -169,7 +201,7 @@ describe('Diffs tree list component', () => { store.state.diffs.renderTreeList = false; await nextTick(); - expect(getScroller().props('items')).toHaveLength(3); + expect(getScroller().props('items')).toHaveLength(5); }); }); @@ -188,4 +220,59 @@ describe('Diffs tree list component', () => { expect(getFileRow().props('viewedFiles')).toBe(viewedDiffFileIds); }); }); + + describe('pinned file', () => { + const filePaths = [ + ['nested-1.rb', 'folder/sub-folder/'], + ['nested-2.rb', 'folder/sub-folder/'], + ['nested-3.rb', 'folder/sub-folder/'], + ['1.rb', 'folder/'], + ['2.rb', 'folder/'], + ['3.rb', 'folder/'], + ['single.rb', 'folder-single/'], + ['root-first.rb'], + ['root-last.rb'], + ]; + + const pinFile = (fileHash) => { + store.commit(`diffs/${SET_PINNED_FILE_HASH}`, fileHash); + }; + + const setupFiles = (diffFiles) => { + const { treeEntries, tree } = generateTreeList(diffFiles); + store.commit(`diffs/${SET_DIFF_FILES}`, diffFiles); + store.commit(`diffs/${SET_TREE_DATA}`, { + treeEntries, + tree: sortTree(tree), + }); + }; + + const createFile = (name, path = '') => ({ + file_hash: name, + path: `${path}${name}`, + new_path: `${path}${name}`, + file_path: `${path}${name}`, + }); + + beforeEach(() => { + createComponent(); + setupFiles(filePaths.map(([name, path]) => createFile(name, path))); + }); + + describe('files in folders', () => { + it.each(filePaths.map((path) => path[0]))('pins %s file', async (pinnedFile) => { + pinFile(pinnedFile); + await nextTick(); + const items = getScroller().props('items'); + expect( + items.map( + (item) => + `${'─'.repeat(item.level * 2)}${item.type === 'tree' ? '📁' : ''}${ + item.name || item.path + }`, + ), + ).toMatchSnapshot(); + }); + }); + }); }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index be3b30e8e7a..c68f3c5991e 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -11,7 +11,7 @@ import { PARALLEL_DIFF_VIEW_TYPE, EVT_MR_PREPARED, } from '~/diffs/constants'; -import { LOAD_SINGLE_DIFF_FAILED, BUILDING_YOUR_MR, SOMETHING_WENT_WRONG } from '~/diffs/i18n'; +import { BUILDING_YOUR_MR, SOMETHING_WENT_WRONG } from '~/diffs/i18n'; import * as diffActions from '~/diffs/store/actions'; import * as types from '~/diffs/store/mutation_types'; import * as utils from '~/diffs/store/utils'; @@ -28,6 +28,8 @@ import { 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 { diffMetadata } from '../mock_data/diff_metadata'; jest.mock('~/alert'); @@ -37,6 +39,8 @@ jest.mock('~/lib/utils/secret_detection', () => ({ containsSensitiveToken: jest.requireActual('~/lib/utils/secret_detection').containsSensitiveToken, })); +const endpointDiffForPath = '/diffs/set/endpoint/path'; + describe('DiffsStoreActions', () => { let mock; @@ -78,7 +82,6 @@ describe('DiffsStoreActions', () => { const endpoint = '/diffs/set/endpoint'; const endpointMetadata = '/diffs/set/endpoint/metadata'; const endpointBatch = '/diffs/set/endpoint/batch'; - const endpointDiffForPath = '/diffs/set/endpoint/path'; const endpointCoverage = '/diffs/set/coverage_reports'; const projectPath = '/root/project'; const dismissEndpoint = '/-/user_callouts'; @@ -181,7 +184,6 @@ describe('DiffsStoreActions', () => { w: '1', view: 'inline', }; - const endpointDiffForPath = '/diffs/set/endpoint/path'; const diffForPath = mergeUrlParams(defaultParams, endpointDiffForPath); const treeEntry = { fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818', @@ -350,7 +352,6 @@ describe('DiffsStoreActions', () => { w: '1', view: 'inline', }; - const endpointDiffForPath = '/diffs/set/endpoint/path'; const diffForPath = mergeUrlParams(defaultParams, endpointDiffForPath); const treeEntry = { fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818', @@ -490,8 +491,8 @@ describe('DiffsStoreActions', () => { describe('fetchDiffFilesBatch', () => { it('should fetch batch diff files', () => { const endpointBatch = '/fetch/diffs_batch'; - const res1 = { diff_files: [{ file_hash: 'test' }], pagination: { total_pages: 7 } }; - const res2 = { diff_files: [{ file_hash: 'test2' }], pagination: { total_pages: 7 } }; + 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( @@ -520,7 +521,7 @@ describe('DiffsStoreActions', () => { return testAction( diffActions.fetchDiffFilesBatch, - {}, + undefined, { endpointBatch, diffViewType: 'inline', diffFiles: [], perPage: 5 }, [ { type: types.SET_BATCH_LOADING_STATE, payload: 'loading' }, @@ -532,7 +533,6 @@ describe('DiffsStoreActions', () => { { type: types.SET_BATCH_LOADING_STATE, payload: 'loaded' }, { type: types.SET_CURRENT_DIFF_FILE, payload: 'test2' }, { type: types.SET_RETRIEVING_BATCHES, payload: false }, - { type: types.SET_BATCH_LOADING_STATE, payload: 'error' }, ], [], ); @@ -690,7 +690,7 @@ describe('DiffsStoreActions', () => { describe('setHighlightedRow', () => { it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => { - return testAction(diffActions.setHighlightedRow, 'ABC_123', {}, [ + return testAction(diffActions.setHighlightedRow, { lineCode: 'ABC_123' }, {}, [ { type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' }, { type: types.SET_CURRENT_DIFF_FILE, payload: 'ABC' }, ]); @@ -1310,14 +1310,17 @@ describe('DiffsStoreActions', () => { diffActions.goToFile({ state, dispatch, getters, commit }, file); expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash); - expect(dispatch).toHaveBeenCalledTimes(0); + expect(dispatch).not.toHaveBeenCalledWith('fetchFileByFile'); }); describe('when the tree entry has not been loaded', () => { it('updates location hash', () => { diffActions.goToFile({ state, commit, getters, dispatch }, file); - expect(document.location.hash).toBe('#test'); + 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 () => { @@ -1333,21 +1336,12 @@ describe('DiffsStoreActions', () => { expect(commonUtils.scrollToElement).toHaveBeenCalledWith('.diff-files-holder', { duration: 0, }); - expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledWith('fetchFileByFile'); }); - it('shows an alert when there was an error fetching the file', async () => { - dispatch = jest.fn().mockRejectedValue(); - + it('unpins the file', () => { diffActions.goToFile({ state, commit, getters, dispatch }, file); - - // Wait for the fetchFileByFile dispatch to return, to trigger the catch - await waitForPromises(); - - expect(createAlert).toHaveBeenCalledTimes(1); - expect(createAlert).toHaveBeenCalledWith({ - message: expect.stringMatching(LOAD_SINGLE_DIFF_FAILED), - }); + expect(dispatch).toHaveBeenCalledWith('unpinFile'); }); }); }); @@ -1969,7 +1963,7 @@ describe('DiffsStoreActions', () => { 0, { flatBlobsList: [{ fileHash: '123' }] }, [{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }], - [], + [{ type: 'unpinFile' }], ); }); @@ -1979,7 +1973,7 @@ describe('DiffsStoreActions', () => { 0, { viewDiffsFileByFile: true, flatBlobsList: [{ fileHash: '123' }] }, [{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }], - [{ type: 'fetchFileByFile' }], + [{ type: 'unpinFile' }, { type: 'fetchFileByFile' }], ); }); }); @@ -2120,4 +2114,84 @@ describe('DiffsStoreActions', () => { ); }); }); + + describe('fetchPinnedFile', () => { + it('fetches pinned file', async () => { + const pinnedFileHref = `${TEST_HOST}/pinned-file`; + const pinnedFile = getDiffFileMock(); + const diffFiles = [pinnedFile]; + const hubSpy = jest.spyOn(diffsEventHub, '$emit'); + mock.onGet(new RegExp(pinnedFileHref)).reply(HTTP_STATUS_OK, { diff_files: diffFiles }); + + await testAction( + diffActions.fetchPinnedFile, + pinnedFileHref, + {}, + [ + { type: types.SET_BATCH_LOADING_STATE, payload: 'loading' }, + { type: types.SET_RETRIEVING_BATCHES, payload: true }, + { + type: types.SET_DIFF_DATA_BATCH, + payload: { diff_files: diffFiles, updatePosition: false }, + }, + { type: types.SET_PINNED_FILE_HASH, payload: pinnedFile.file_hash }, + { type: types.SET_CURRENT_DIFF_FILE, payload: pinnedFile.file_hash }, + { type: types.SET_BATCH_LOADING_STATE, payload: 'loaded' }, + { type: types.SET_RETRIEVING_BATCHES, payload: false }, + ], + [], + ); + + jest.runAllTimers(); + expect(hubSpy).toHaveBeenCalledWith('diffFilesModified'); + expect(handleLocationHash).toHaveBeenCalled(); + }); + + it('handles load error', async () => { + const pinnedFileHref = `${TEST_HOST}/pinned-file`; + const hubSpy = jest.spyOn(diffsEventHub, '$emit'); + mock.onGet(new RegExp(pinnedFileHref)).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + + try { + await testAction( + diffActions.fetchPinnedFile, + pinnedFileHref, + {}, + [ + { type: types.SET_BATCH_LOADING_STATE, payload: 'loading' }, + { type: types.SET_RETRIEVING_BATCHES, payload: true }, + { type: types.SET_BATCH_LOADING_STATE, payload: 'error' }, + { type: 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(); + }); + }); + + describe('unpinFile', () => { + it('unpins pinned file', () => { + const pinnedFile = getDiffFileMock(); + setWindowLocation(`${TEST_HOST}/?pin=${pinnedFile.file_hash}#${pinnedFile.file_hash}_10_10`); + testAction( + diffActions.unpinFile, + undefined, + { pinnedFile }, + [{ type: types.SET_PINNED_FILE_HASH, payload: null }], + [], + ); + expect(window.location.hash).toBe(''); + expect(window.location.search).toBe(''); + }); + + it('does nothing when no pinned file present', () => { + testAction(diffActions.unpinFile, undefined, {}, [], []); + }); + }); }); diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js index 8097f0976f6..cb0f40534fe 100644 --- a/spec/frontend/diffs/store/getters_spec.js +++ b/spec/frontend/diffs/store/getters_spec.js @@ -1,6 +1,7 @@ import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; import * as getters from '~/diffs/store/getters'; import state from '~/diffs/store/modules/diff_state'; +import { getDiffFileMock } from 'jest/diffs/mock_data/diff_file'; import discussion from '../mock_data/diff_discussions'; describe('Diffs Module Getters', () => { @@ -495,4 +496,35 @@ describe('Diffs Module Getters', () => { }, ); }); + + describe('diffFiles', () => { + it('proxies diffFiles state', () => { + const diffFiles = [getDiffFileMock()]; + expect(getters.diffFiles({ diffFiles }, {})).toBe(diffFiles); + }); + + it('pins the file', () => { + const pinnedFile = getDiffFileMock(); + const regularFile = getDiffFileMock(); + const diffFiles = [regularFile, pinnedFile]; + expect(getters.diffFiles({ diffFiles }, { pinnedFile })).toStrictEqual([ + pinnedFile, + regularFile, + ]); + }); + }); + + describe('pinnedFile', () => { + it('returns pinnedFile', () => { + const pinnedFile = getDiffFileMock(); + const diffFiles = [pinnedFile]; + expect(getters.pinnedFile({ diffFiles, pinnedFileHash: pinnedFile.file_hash }, {})).toBe( + pinnedFile, + ); + }); + + it('returns null if no pinned file is set', () => { + expect(getters.pinnedFile({}, {})).toBe(null); + }); + }); }); diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js index a5be41aa69f..8d52cd39542 100644 --- a/spec/frontend/diffs/store/mutations_spec.js +++ b/spec/frontend/diffs/store/mutations_spec.js @@ -92,7 +92,7 @@ describe('DiffsStoreMutations', () => { }); }); - describe('SET_DIFF_DATA_BATCH_DATA', () => { + describe('SET_DIFF_DATA_BATCH', () => { it('should set diff data batch type properly', () => { const mockFile = getDiffFileMock(); const state = { @@ -108,6 +108,39 @@ describe('DiffsStoreMutations', () => { expect(state.diffFiles[0].collapsed).toEqual(false); expect(state.treeEntries[mockFile.file_path].diffLoaded).toBe(true); }); + + it('should update diff position by default', () => { + const mockFile = getDiffFileMock(); + const state = { + diffFiles: [mockFile, { ...mockFile, file_hash: 'foo', file_path: 'foo' }], + treeEntries: { [mockFile.file_path]: { fileHash: mockFile.file_hash } }, + }; + const diffMock = { + diff_files: [mockFile], + }; + + mutations[types.SET_DIFF_DATA_BATCH](state, diffMock); + + expect(state.diffFiles[1].file_hash).toBe(mockFile.file_hash); + expect(state.treeEntries[mockFile.file_path].diffLoaded).toBe(true); + }); + + it('should not update diff position', () => { + const mockFile = getDiffFileMock(); + const state = { + diffFiles: [mockFile, { ...mockFile, file_hash: 'foo', file_path: 'foo' }], + treeEntries: { [mockFile.file_path]: { fileHash: mockFile.file_hash } }, + }; + const diffMock = { + diff_files: [mockFile], + updatePosition: false, + }; + + mutations[types.SET_DIFF_DATA_BATCH](state, diffMock); + + expect(state.diffFiles[0].file_hash).toBe(mockFile.file_hash); + expect(state.treeEntries[mockFile.file_path].diffLoaded).toBe(true); + }); }); describe('SET_COVERAGE_DATA', () => { @@ -122,6 +155,17 @@ describe('DiffsStoreMutations', () => { }); }); + describe('SET_DIFF_TREE_ENTRY', () => { + it('should set tree entry', () => { + const file = getDiffFileMock(); + const state = { treeEntries: { [file.file_path]: {} } }; + + mutations[types.SET_DIFF_TREE_ENTRY](state, file); + + expect(state.treeEntries[file.file_path].diffLoaded).toBe(true); + }); + }); + describe('SET_DIFF_VIEW_TYPE', () => { it('should set diff view type properly', () => { const state = {}; @@ -1076,4 +1120,15 @@ describe('DiffsStoreMutations', () => { expect(state.diffFiles[0].viewer.forceOpen).toBe(true); }); }); + + describe('SET_PINNED_FILE_HASH', () => { + it('set pinned file hash', () => { + const state = {}; + const file = getDiffFileMock(); + + mutations[types.SET_PINNED_FILE_HASH](state, file.file_hash); + + expect(state.pinnedFileHash).toBe(file.file_hash); + }); + }); }); diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js index 6331269d6e8..019ed663d82 100644 --- a/spec/frontend/diffs/store/utils_spec.js +++ b/spec/frontend/diffs/store/utils_spec.js @@ -476,6 +476,17 @@ describe('DiffsStoreUtils', () => { expect(updatedFilesList).toEqual([mock, fakeNewFile]); }); + it('updates diff position', () => { + const priorFiles = [mock, { ...mock, file_hash: 'foo', file_path: 'foo' }]; + const updatedFilesList = utils.prepareDiffData({ + diff: { diff_files: [mock] }, + priorFiles, + updatePosition: true, + }); + + expect(updatedFilesList[1].file_hash).toEqual(mock.file_hash); + }); + it('completes an existing split diff without overwriting existing diffs', () => { // The current state has a file that has only loaded inline lines const priorFiles = [{ ...mock }]; |