diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 11:43:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 11:43:02 +0300 |
commit | d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch) | |
tree | 2341ef426af70ad1e289c38036737e04b0aa5007 /spec/frontend/repository | |
parent | d6e514dd13db8947884cd58fe2a9c2a063400a9b (diff) |
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'spec/frontend/repository')
12 files changed, 653 insertions, 45 deletions
diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js new file mode 100644 index 00000000000..d924974aede --- /dev/null +++ b/spec/frontend/repository/commits_service_spec.js @@ -0,0 +1,84 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service'; +import httpStatus from '~/lib/utils/http_status'; +import createFlash from '~/flash'; +import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants'; + +jest.mock('~/flash'); + +describe('commits service', () => { + let mock; + const url = `${gon.relative_url_root || ''}/my-project/-/refs/main/logs_tree/`; + + beforeEach(() => { + mock = new MockAdapter(axios); + + mock.onGet(url).reply(httpStatus.OK, [], {}); + + jest.spyOn(axios, 'get'); + }); + + afterEach(() => { + mock.restore(); + resetRequestedCommits(); + }); + + const requestCommits = (offset, project = 'my-project', path = '', ref = 'main') => + loadCommits(project, path, ref, offset); + + it('calls axios get', async () => { + const offset = 10; + const project = 'my-project'; + const path = 'my-path'; + const ref = 'my-ref'; + const testUrl = `${gon.relative_url_root || ''}/${project}/-/refs/${ref}/logs_tree/${path}`; + + await requestCommits(offset, project, path, ref); + + expect(axios.get).toHaveBeenCalledWith(testUrl, { params: { format: 'json', offset } }); + }); + + it('encodes the path correctly', async () => { + await requestCommits(1, 'some-project', 'with $peci@l ch@rs/'); + + const encodedUrl = '/some-project/-/refs/main/logs_tree/with%20%24peci%40l%20ch%40rs%2F'; + expect(axios.get).toHaveBeenCalledWith(encodedUrl, expect.anything()); + }); + + it('calls axios get once per batch', async () => { + await Promise.all([requestCommits(0), requestCommits(1), requestCommits(23)]); + + expect(axios.get.mock.calls.length).toEqual(1); + }); + + it('calls axios get twice if an offset is larger than 25', async () => { + await requestCommits(100); + + expect(axios.get.mock.calls[0][1]).toEqual({ params: { format: 'json', offset: 75 } }); + expect(axios.get.mock.calls[1][1]).toEqual({ params: { format: 'json', offset: 100 } }); + }); + + it('updates the list of requested offsets', async () => { + await requestCommits(200); + + expect(isRequested(200)).toBe(true); + }); + + it('resets the list of requested offsets', async () => { + await requestCommits(300); + + resetRequestedCommits(); + expect(isRequested(300)).toBe(false); + }); + + it('calls `createFlash` when the request fails', async () => { + const invalidPath = '/#@ some/path'; + const invalidUrl = `${url}${invalidPath}`; + mock.onGet(invalidUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, [], {}); + + await requestCommits(1, 'my-project', invalidPath); + + expect(createFlash).toHaveBeenCalledWith({ message: I18N_COMMIT_DATA_FETCH_ERROR }); + }); +}); diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js index 8331adcdfc2..59db537282b 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -11,13 +11,18 @@ import BlobHeader from '~/blob/components/blob_header.vue'; import BlobButtonGroup from '~/repository/components/blob_button_group.vue'; import BlobContentViewer from '~/repository/components/blob_content_viewer.vue'; import BlobEdit from '~/repository/components/blob_edit.vue'; +import ForkSuggestion from '~/repository/components/fork_suggestion.vue'; import { loadViewer, viewerProps } from '~/repository/components/blob_viewers'; import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue'; import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue'; import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue'; import blobInfoQuery from '~/repository/queries/blob_info.query.graphql'; +import { redirectTo } from '~/lib/utils/url_utility'; +import { isLoggedIn } from '~/lib/utils/common_utils'; jest.mock('~/repository/components/blob_viewers'); +jest.mock('~/lib/utils/url_utility'); +jest.mock('~/lib/utils/common_utils'); let wrapper; let mockResolver; @@ -34,12 +39,14 @@ const simpleMockData = { webPath: 'some_file.js', editBlobPath: 'some_file.js/edit', ideEditPath: 'some_file.js/ide/edit', + forkAndEditPath: 'some_file.js/fork/edit', + ideForkAndEditPath: 'some_file.js/fork/ide', + canModifyBlob: true, storedExternally: false, rawPath: 'some_file.js', externalStorageUrl: 'some_file.js', replacePath: 'some_file.js/replace', deletePath: 'some_file.js/delete', - forkPath: 'some_file.js/fork', simpleViewer: { fileType: 'text', tooLarge: false, @@ -62,6 +69,8 @@ const projectMockData = { userPermissions: { pushCode: true, downloadCode: true, + createMergeRequestIn: true, + forkProject: true, }, repository: { empty: false, @@ -82,6 +91,8 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => { emptyRepo = defaultEmptyRepo, canPushCode = defaultPushCode, canDownloadCode = defaultDownloadCode, + createMergeRequestIn = projectMockData.userPermissions.createMergeRequestIn, + forkProject = projectMockData.userPermissions.forkProject, pathLocks = [], } = mockData; @@ -89,7 +100,12 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => { data: { project: { id: '1234', - userPermissions: { pushCode: canPushCode, downloadCode: canDownloadCode }, + userPermissions: { + pushCode: canPushCode, + downloadCode: canDownloadCode, + createMergeRequestIn, + forkProject, + }, pathLocks: { nodes: pathLocks, }, @@ -158,9 +174,16 @@ describe('Blob content viewer component', () => { const findBlobEdit = () => wrapper.findComponent(BlobEdit); const findBlobContent = () => wrapper.findComponent(BlobContent); const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup); + const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion); + + beforeEach(() => { + gon.features = { refactorTextViewer: true }; + isLoggedIn.mockReturnValue(true); + }); afterEach(() => { wrapper.destroy(); + mockAxios.reset(); }); it('renders a GlLoadingIcon component', () => { @@ -183,7 +206,6 @@ describe('Blob content viewer component', () => { it('renders a BlobContent component', () => { expect(findBlobContent().props('loading')).toEqual(false); - expect(findBlobContent().props('content')).toEqual('raw content'); expect(findBlobContent().props('isRawContent')).toBe(true); expect(findBlobContent().props('activeViewer')).toEqual({ fileType: 'text', @@ -192,6 +214,16 @@ describe('Blob content viewer component', () => { renderError: null, }); }); + + describe('legacy viewers', () => { + it('loads a legacy viewer when a viewer component is not available', async () => { + createComponentWithApollo({ blobs: { ...simpleMockData, fileType: 'unknown' } }); + await waitForPromises(); + + expect(mockAxios.history.get).toHaveLength(1); + expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple'); + }); + }); }); describe('rich viewer', () => { @@ -210,7 +242,6 @@ describe('Blob content viewer component', () => { it('renders a BlobContent component', () => { expect(findBlobContent().props('loading')).toEqual(false); - expect(findBlobContent().props('content')).toEqual('raw content'); expect(findBlobContent().props('isRawContent')).toBe(true); expect(findBlobContent().props('activeViewer')).toEqual({ fileType: 'markup', @@ -241,18 +272,12 @@ describe('Blob content viewer component', () => { }); describe('legacy viewers', () => { - it('does not load a legacy viewer when a rich viewer is not available', async () => { - createComponentWithApollo({ blobs: simpleMockData }); - await waitForPromises(); - - expect(mockAxios.history.get).toHaveLength(0); - }); - - it('loads a legacy viewer when a rich viewer is available', async () => { - createComponentWithApollo({ blobs: richMockData }); + it('loads a legacy viewer when a viewer component is not available', async () => { + createComponentWithApollo({ blobs: { ...richMockData, fileType: 'unknown' } }); await waitForPromises(); expect(mockAxios.history.get).toHaveLength(1); + expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=rich'); }); }); @@ -462,7 +487,7 @@ describe('Blob content viewer component', () => { }); it('does not render if not logged in', async () => { - window.gon.current_user_id = null; + isLoggedIn.mockReturnValueOnce(false); fullFactory({ mockData: { blobInfo: simpleMockData }, @@ -506,4 +531,60 @@ describe('Blob content viewer component', () => { ); }); }); + + describe('edit blob', () => { + beforeEach(() => { + fullFactory({ + mockData: { blobInfo: simpleMockData }, + stubs: { + BlobContent: true, + BlobReplace: true, + }, + }); + }); + + it('simple edit redirects to the simple editor', () => { + findBlobEdit().vm.$emit('edit', 'simple'); + expect(redirectTo).toHaveBeenCalledWith(simpleMockData.editBlobPath); + }); + + it('IDE edit redirects to the IDE editor', () => { + findBlobEdit().vm.$emit('edit', 'ide'); + expect(redirectTo).toHaveBeenCalledWith(simpleMockData.ideEditPath); + }); + + it.each` + loggedIn | canModifyBlob | createMergeRequestIn | forkProject | showForkSuggestion + ${true} | ${false} | ${true} | ${true} | ${true} + ${false} | ${false} | ${true} | ${true} | ${false} + ${true} | ${true} | ${false} | ${true} | ${false} + ${true} | ${true} | ${true} | ${false} | ${false} + `( + 'shows/hides a fork suggestion according to a set of conditions', + async ({ + loggedIn, + canModifyBlob, + createMergeRequestIn, + forkProject, + showForkSuggestion, + }) => { + isLoggedIn.mockReturnValueOnce(loggedIn); + fullFactory({ + mockData: { + blobInfo: { ...simpleMockData, canModifyBlob }, + project: { userPermissions: { createMergeRequestIn, forkProject } }, + }, + stubs: { + BlobContent: true, + BlobButtonGroup: true, + }, + }); + + findBlobEdit().vm.$emit('edit', 'simple'); + await nextTick(); + + expect(findForkSuggestion().exists()).toBe(showForkSuggestion); + }, + ); + }); }); diff --git a/spec/frontend/repository/components/blob_edit_spec.js b/spec/frontend/repository/components/blob_edit_spec.js index 11739674bc9..e2de7bc2957 100644 --- a/spec/frontend/repository/components/blob_edit_spec.js +++ b/spec/frontend/repository/components/blob_edit_spec.js @@ -7,6 +7,7 @@ const DEFAULT_PROPS = { editPath: 'some_file.js/edit', webIdePath: 'some_file.js/ide/edit', showEditButton: true, + needsToFork: false, }; describe('BlobEdit component', () => { @@ -56,7 +57,6 @@ describe('BlobEdit component', () => { it('renders the Edit button', () => { createComponent(); - expect(findEditButton().attributes('href')).toBe(DEFAULT_PROPS.editPath); expect(findEditButton().text()).toBe('Edit'); expect(findEditButton()).not.toBeDisabled(); }); @@ -64,7 +64,6 @@ describe('BlobEdit component', () => { it('renders the Web IDE button', () => { createComponent(); - expect(findWebIdeButton().attributes('href')).toBe(DEFAULT_PROPS.webIdePath); expect(findWebIdeButton().text()).toBe('Web IDE'); expect(findWebIdeButton()).not.toBeDisabled(); }); @@ -72,13 +71,14 @@ describe('BlobEdit component', () => { it('renders WebIdeLink component', () => { createComponent(true); - const { editPath: editUrl, webIdePath: webIdeUrl } = DEFAULT_PROPS; + const { editPath: editUrl, webIdePath: webIdeUrl, needsToFork } = DEFAULT_PROPS; expect(findWebIdeLink().props()).toMatchObject({ editUrl, webIdeUrl, isBlob: true, showEditButton: true, + needsToFork, }); }); diff --git a/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js new file mode 100644 index 00000000000..34448c03b31 --- /dev/null +++ b/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js @@ -0,0 +1,22 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import VideoViewer from '~/repository/components/blob_viewers/video_viewer.vue'; + +describe('Video Viewer', () => { + let wrapper; + + const propsData = { url: 'some/video.mp4' }; + + const createComponent = () => { + wrapper = shallowMountExtended(VideoViewer, { propsData }); + }; + + const findVideo = () => wrapper.findByTestId('video'); + + it('renders a Video element', () => { + createComponent(); + + expect(findVideo().exists()).toBe(true); + expect(findVideo().attributes('src')).toBe(propsData.url); + expect(findVideo().attributes('controls')).not.toBeUndefined(); + }); +}); diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js index 0733cffe4f4..eb957c635ac 100644 --- a/spec/frontend/repository/components/breadcrumbs_spec.js +++ b/spec/frontend/repository/components/breadcrumbs_spec.js @@ -2,6 +2,7 @@ import { GlDropdown } from '@gitlab/ui'; import { shallowMount, RouterLinkStub } from '@vue/test-utils'; import Breadcrumbs from '~/repository/components/breadcrumbs.vue'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; +import NewDirectoryModal from '~/repository/components/new_directory_modal.vue'; const defaultMockRoute = { name: 'blobPath', @@ -10,7 +11,7 @@ const defaultMockRoute = { describe('Repository breadcrumbs component', () => { let wrapper; - const factory = (currentPath, extraProps = {}, mockRoute = {}) => { + const factory = (currentPath, extraProps = {}, mockRoute = {}, newDirModal = true) => { const $apollo = { queries: { userPermissions: { @@ -34,10 +35,12 @@ describe('Repository breadcrumbs component', () => { }, $apollo, }, + provide: { glFeatures: { newDirModal } }, }); }; const findUploadBlobModal = () => wrapper.find(UploadBlobModal); + const findNewDirectoryModal = () => wrapper.find(NewDirectoryModal); afterEach(() => { wrapper.destroy(); @@ -121,4 +124,37 @@ describe('Repository breadcrumbs component', () => { expect(findUploadBlobModal().exists()).toBe(true); }); }); + + describe('renders the new directory modal', () => { + describe('with the feature flag enabled', () => { + beforeEach(() => { + window.gon.features = { + newDirModal: true, + }; + factory('/', { canEditTree: true }); + }); + + it('does not render the modal while loading', () => { + expect(findNewDirectoryModal().exists()).toBe(false); + }); + + it('renders the modal once loaded', async () => { + wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } }); + + await wrapper.vm.$nextTick(); + + expect(findNewDirectoryModal().exists()).toBe(true); + }); + }); + + describe('with the feature flag disabled', () => { + it('does not render the modal', () => { + window.gon.features = { + newDirModal: false, + }; + factory('/', { canEditTree: true }, {}, {}, false); + expect(findNewDirectoryModal().exists()).toBe(false); + }); + }); + }); }); diff --git a/spec/frontend/repository/components/fork_suggestion_spec.js b/spec/frontend/repository/components/fork_suggestion_spec.js new file mode 100644 index 00000000000..36a48a3fdb8 --- /dev/null +++ b/spec/frontend/repository/components/fork_suggestion_spec.js @@ -0,0 +1,44 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ForkSuggestion from '~/repository/components/fork_suggestion.vue'; + +const DEFAULT_PROPS = { forkPath: 'some_file.js/fork' }; + +describe('ForkSuggestion component', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(ForkSuggestion, { + propsData: { ...DEFAULT_PROPS }, + }); + }; + + beforeEach(() => createComponent()); + + afterEach(() => wrapper.destroy()); + + const { i18n } = ForkSuggestion; + const findMessage = () => wrapper.findByTestId('message'); + const findForkButton = () => wrapper.findByTestId('fork'); + const findCancelButton = () => wrapper.findByTestId('cancel'); + + it('renders a message', () => { + expect(findMessage().text()).toBe(i18n.message); + }); + + it('renders a Fork button', () => { + const forkButton = findForkButton(); + + expect(forkButton.text()).toBe(i18n.fork); + expect(forkButton.attributes('href')).toBe(DEFAULT_PROPS.forkPath); + }); + + it('renders a Cancel button', () => { + expect(findCancelButton().text()).toBe(i18n.cancel); + }); + + it('emits a cancel event when Cancel button is clicked', () => { + findCancelButton().vm.$emit('click'); + + expect(wrapper.emitted('cancel')).toEqual([[]]); + }); +}); diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js new file mode 100644 index 00000000000..fe7f024e3ea --- /dev/null +++ b/spec/frontend/repository/components/new_directory_modal_spec.js @@ -0,0 +1,203 @@ +import { GlModal, GlFormTextarea, GlToggle } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import httpStatusCodes from '~/lib/utils/http_status'; +import { visitUrl } from '~/lib/utils/url_utility'; +import NewDirectoryModal from '~/repository/components/new_directory_modal.vue'; + +jest.mock('~/flash'); +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), +})); + +const initialProps = { + modalTitle: 'Create New Directory', + modalId: 'modal-new-directory', + commitMessage: 'Add new directory', + targetBranch: 'some-target-branch', + originalBranch: 'master', + canPushCode: true, + path: 'create_dir', +}; + +const defaultFormValue = { + dirName: 'foo', + originalBranch: initialProps.originalBranch, + branchName: initialProps.targetBranch, + commitMessage: initialProps.commitMessage, + createNewMr: true, +}; + +describe('NewDirectoryModal', () => { + let wrapper; + let mock; + + const createComponent = (props = {}) => { + wrapper = shallowMount(NewDirectoryModal, { + propsData: { + ...initialProps, + ...props, + }, + attrs: { + static: true, + visible: true, + }, + }); + }; + + const findModal = () => wrapper.findComponent(GlModal); + const findDirName = () => wrapper.find('[name="dir_name"]'); + const findBranchName = () => wrapper.find('[name="branch_name"]'); + const findCommitMessage = () => wrapper.findComponent(GlFormTextarea); + const findMrToggle = () => wrapper.findComponent(GlToggle); + + const fillForm = async (inputValue = {}) => { + const { + dirName = defaultFormValue.dirName, + branchName = defaultFormValue.branchName, + commitMessage = defaultFormValue.commitMessage, + createNewMr = true, + } = inputValue; + + await findDirName().vm.$emit('input', dirName); + await findBranchName().vm.$emit('input', branchName); + await findCommitMessage().vm.$emit('input', commitMessage); + await findMrToggle().vm.$emit('change', createNewMr); + await nextTick; + }; + + const submitForm = async () => { + const mockEvent = { preventDefault: jest.fn() }; + findModal().vm.$emit('primary', mockEvent); + await waitForPromises(); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders modal component', () => { + createComponent(); + + const { modalTitle: title } = initialProps; + + expect(findModal().props()).toMatchObject({ + title, + size: 'md', + actionPrimary: { + text: NewDirectoryModal.i18n.PRIMARY_OPTIONS_TEXT, + }, + actionCancel: { + text: 'Cancel', + }, + }); + }); + + describe('form', () => { + it.each` + component | defaultValue | canPushCode | targetBranch | originalBranch | exist + ${findDirName} | ${undefined} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${findBranchName} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${findBranchName} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false} + ${findCommitMessage} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${findMrToggle} | ${'true'} | ${true} | ${'new-target-branch'} | ${'master'} | ${true} + ${findMrToggle} | ${'true'} | ${true} | ${'master'} | ${'master'} | ${true} + `( + 'has the correct form fields ', + ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => { + createComponent({ + canPushCode, + targetBranch, + originalBranch, + }); + const formField = component(); + + if (!exist) { + expect(formField.exists()).toBe(false); + return; + } + + expect(formField.exists()).toBe(true); + expect(formField.attributes('value')).toBe(defaultValue); + }, + ); + }); + + describe('form submission', () => { + beforeEach(async () => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('valid form', () => { + beforeEach(() => { + createComponent(); + }); + + it('passes the formData', async () => { + const { + dirName, + branchName, + commitMessage, + originalBranch, + createNewMr, + } = defaultFormValue; + mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {}); + await fillForm(); + await submitForm(); + + expect(mock.history.post[0].data.get('dir_name')).toEqual(dirName); + expect(mock.history.post[0].data.get('branch_name')).toEqual(branchName); + expect(mock.history.post[0].data.get('commit_message')).toEqual(commitMessage); + expect(mock.history.post[0].data.get('original_branch')).toEqual(originalBranch); + expect(mock.history.post[0].data.get('create_merge_request')).toEqual(String(createNewMr)); + }); + + it('does not submit "create_merge_request" formData if createNewMr is not checked', async () => { + mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {}); + await fillForm({ createNewMr: false }); + await submitForm(); + expect(mock.history.post[0].data.get('create_merge_request')).toBeNull(); + }); + + it('redirects to the new directory', async () => { + const response = { filePath: 'new-dir-path' }; + mock.onPost(initialProps.path).reply(httpStatusCodes.OK, response); + + await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' }); + await submitForm(); + + expect(visitUrl).toHaveBeenCalledWith(response.filePath); + }); + }); + + describe('invalid form', () => { + beforeEach(() => { + createComponent(); + }); + + it('disables submit button', async () => { + await fillForm({ dirName: '', branchName: '', commitMessage: '' }); + expect(findModal().props('actionPrimary').attributes[0].disabled).toBe(true); + }); + + it('creates a flash error', async () => { + mock.onPost(initialProps.path).timeout(); + + await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' }); + await submitForm(); + + expect(createFlash).toHaveBeenCalledWith({ + message: NewDirectoryModal.i18n.ERROR_MESSAGE, + }); + }); + }); + }); +}); diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap index 6f461f4c69b..26064e9b248 100644 --- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap +++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap @@ -31,25 +31,36 @@ exports[`Repository table row component renders a symlink table row 1`] = ` <!----> - <!----> + <gl-icon-stub + class="ml-1" + name="lock" + size="12" + title="Locked by Root" + /> </td> <td class="d-none d-sm-table-cell tree-commit cursor-default" > - <gl-skeleton-loading-stub - class="h-auto" - lines="1" + <gl-link-stub + class="str-truncated-100 tree-commit-link" /> + + <gl-intersection-observer-stub> + <!----> + </gl-intersection-observer-stub> </td> <td class="tree-time-ago text-right cursor-default" > - <gl-skeleton-loading-stub - class="ml-auto h-auto w-50" - lines="1" + <timeago-tooltip-stub + cssclass="" + time="2019-01-01" + tooltipplacement="top" /> + + <!----> </td> </tr> `; @@ -85,25 +96,36 @@ exports[`Repository table row component renders table row 1`] = ` <!----> - <!----> + <gl-icon-stub + class="ml-1" + name="lock" + size="12" + title="Locked by Root" + /> </td> <td class="d-none d-sm-table-cell tree-commit cursor-default" > - <gl-skeleton-loading-stub - class="h-auto" - lines="1" + <gl-link-stub + class="str-truncated-100 tree-commit-link" /> + + <gl-intersection-observer-stub> + <!----> + </gl-intersection-observer-stub> </td> <td class="tree-time-ago text-right cursor-default" > - <gl-skeleton-loading-stub - class="ml-auto h-auto w-50" - lines="1" + <timeago-tooltip-stub + cssclass="" + time="2019-01-01" + tooltipplacement="top" /> + + <!----> </td> </tr> `; @@ -139,25 +161,36 @@ exports[`Repository table row component renders table row for path with special <!----> - <!----> + <gl-icon-stub + class="ml-1" + name="lock" + size="12" + title="Locked by Root" + /> </td> <td class="d-none d-sm-table-cell tree-commit cursor-default" > - <gl-skeleton-loading-stub - class="h-auto" - lines="1" + <gl-link-stub + class="str-truncated-100 tree-commit-link" /> + + <gl-intersection-observer-stub> + <!----> + </gl-intersection-observer-stub> </td> <td class="tree-time-ago text-right cursor-default" > - <gl-skeleton-loading-stub - class="ml-auto h-auto w-50" - lines="1" + <timeago-tooltip-stub + cssclass="" + time="2019-01-01" + tooltipplacement="top" /> + + <!----> </td> </tr> `; diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js index e9e51abaf0f..c8dddefc4f2 100644 --- a/spec/frontend/repository/components/table/index_spec.js +++ b/spec/frontend/repository/components/table/index_spec.js @@ -34,17 +34,45 @@ const MOCK_BLOBS = [ }, ]; -function factory({ path, isLoading = false, hasMore = true, entries = {} }) { +const MOCK_COMMITS = [ + { + fileName: 'blob.md', + type: 'blob', + commit: { + message: 'Updated blob.md', + }, + }, + { + fileName: 'blob2.md', + type: 'blob', + commit: { + message: 'Updated blob2.md', + }, + }, + { + fileName: 'blob3.md', + type: 'blob', + commit: { + message: 'Updated blob3.md', + }, + }, +]; + +function factory({ path, isLoading = false, hasMore = true, entries = {}, commits = [] }) { vm = shallowMount(Table, { propsData: { path, isLoading, entries, hasMore, + commits, }, mocks: { $apollo, }, + provide: { + glFeatures: { lazyLoadCommits: true }, + }, }); } @@ -82,12 +110,15 @@ describe('Repository table component', () => { entries: { blobs: MOCK_BLOBS, }, + commits: MOCK_COMMITS, }); const rows = vm.findAll(TableRow); expect(rows.length).toEqual(3); expect(rows.at(2).attributes().mode).toEqual('120000'); + expect(rows.at(2).props().rowNumber).toBe(2); + expect(rows.at(2).props().commitInfo).toEqual(MOCK_COMMITS[2]); }); describe('Show more button', () => { diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index da28c9873d9..76e9f7da011 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -1,10 +1,12 @@ -import { GlBadge, GlLink, GlIcon } from '@gitlab/ui'; +import { GlBadge, GlLink, GlIcon, GlIntersectionObserver } from '@gitlab/ui'; import { shallowMount, RouterLinkStub } from '@vue/test-utils'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import TableRow from '~/repository/components/table/row.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import { FILE_SYMLINK_MODE } from '~/vue_shared/constants'; +const COMMIT_MOCK = { lockLabel: 'Locked by Root', committedDate: '2019-01-01' }; + let vm; let $router; @@ -20,12 +22,14 @@ function factory(propsData = {}) { projectPath: 'gitlab-org/gitlab-ce', url: `https://test.com`, totalEntries: 10, + commitInfo: COMMIT_MOCK, + rowNumber: 123, }, directives: { GlHoverLoad: createMockDirective(), }, provide: { - glFeatures: { refactorBlobViewer: true }, + glFeatures: { refactorBlobViewer: true, lazyLoadCommits: true }, }, mocks: { $router, @@ -40,6 +44,7 @@ function factory(propsData = {}) { describe('Repository table row component', () => { const findRouterLink = () => vm.find(RouterLinkStub); + const findIntersectionObserver = () => vm.findComponent(GlIntersectionObserver); afterEach(() => { vm.destroy(); @@ -226,8 +231,6 @@ describe('Repository table row component', () => { currentPath: '/', }); - vm.setData({ commit: { lockLabel: 'Locked by Root', committedDate: '2019-01-01' } }); - return vm.vm.$nextTick().then(() => { expect(vm.find(GlIcon).exists()).toBe(true); expect(vm.find(GlIcon).props('name')).toBe('lock'); @@ -246,4 +249,27 @@ describe('Repository table row component', () => { expect(vm.find(FileIcon).props('loading')).toBe(true); }); + + describe('row visibility', () => { + beforeEach(() => { + factory({ + id: '1', + sha: '1', + path: 'test', + type: 'tree', + currentPath: '/', + }); + }); + it('emits a `row-appear` event', () => { + findIntersectionObserver().vm.$emit('appear'); + expect(vm.emitted('row-appear')).toEqual([ + [ + { + hasCommit: true, + rowNumber: 123, + }, + ], + ]); + }); + }); }); diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js index e36287eff29..49397c77215 100644 --- a/spec/frontend/repository/components/tree_content_spec.js +++ b/spec/frontend/repository/components/tree_content_spec.js @@ -3,6 +3,13 @@ import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.g import FilePreview from '~/repository/components/preview/index.vue'; import FileTable from '~/repository/components/table/index.vue'; import TreeContent from '~/repository/components/tree_content.vue'; +import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service'; + +jest.mock('~/repository/commits_service', () => ({ + loadCommits: jest.fn(() => Promise.resolve()), + isRequested: jest.fn(), + resetRequestedCommits: jest.fn(), +})); let vm; let $apollo; @@ -23,6 +30,7 @@ function factory(path, data = () => ({})) { glFeatures: { increasePageSizeExponentially: true, paginatedTreeGraphqlQuery: true, + lazyLoadCommits: true, }, }, }); @@ -45,7 +53,7 @@ describe('Repository table component', () => { expect(vm.find(FilePreview).exists()).toBe(true); }); - it('trigger fetchFiles when mounted', async () => { + it('trigger fetchFiles and resetRequestedCommits when mounted', async () => { factory('/'); jest.spyOn(vm.vm, 'fetchFiles').mockImplementation(() => {}); @@ -53,6 +61,7 @@ describe('Repository table component', () => { await vm.vm.$nextTick(); expect(vm.vm.fetchFiles).toHaveBeenCalled(); + expect(resetRequestedCommits).toHaveBeenCalled(); }); describe('normalizeData', () => { @@ -180,4 +189,15 @@ describe('Repository table component', () => { }); }); }); + + it('loads commit data when row-appear event is emitted', () => { + const path = 'some/path'; + const rowNumber = 1; + + factory(path); + findFileTable().vm.$emit('row-appear', { hasCommit: false, rowNumber }); + + expect(isRequested).toHaveBeenCalledWith(rowNumber); + expect(loadCommits).toHaveBeenCalledWith('', path, '', rowNumber); + }); }); diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js index bb82fa706fd..3f822db601f 100644 --- a/spec/frontend/repository/router_spec.js +++ b/spec/frontend/repository/router_spec.js @@ -24,4 +24,32 @@ describe('Repository router spec', () => { expect(componentsForRoute).toContain(component); } }); + + describe('Storing Web IDE path globally', () => { + const proj = 'foo-bar-group/foo-bar-proj'; + let originalGl; + + beforeEach(() => { + originalGl = window.gl; + }); + + afterEach(() => { + window.gl = originalGl; + }); + + it.each` + path | branch | expectedPath + ${'/'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`} + ${'/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`} + ${'/tree/feat(test)'} | ${'feat(test)'} | ${`/-/ide/project/${proj}/edit/feat(test)/-/`} + ${'/-/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`} + ${'/-/tree/main/app/assets'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/app/assets/`} + ${'/-/blob/main/file.md'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/file.md`} + `('generates the correct Web IDE url for $path', ({ path, branch, expectedPath } = {}) => { + const router = createRouter(proj, branch); + + router.push(path); + expect(window.gl.webIDEPath).toBe(expectedPath); + }); + }); }); |