diff options
Diffstat (limited to 'spec/frontend/repository')
10 files changed, 258 insertions, 19 deletions
diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js index de7c56f239a..e56975d021a 100644 --- a/spec/frontend/repository/commits_service_spec.js +++ b/spec/frontend/repository/commits_service_spec.js @@ -1,9 +1,10 @@ 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 { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { createAlert } from '~/flash'; import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants'; +import { refWithSpecialCharMock } from './mock_data'; jest.mock('~/flash'); @@ -14,7 +15,7 @@ describe('commits service', () => { beforeEach(() => { mock = new MockAdapter(axios); - mock.onGet(url).reply(httpStatus.OK, [], {}); + mock.onGet(url).reply(HTTP_STATUS_OK, [], {}); jest.spyOn(axios, 'get'); }); @@ -39,10 +40,12 @@ describe('commits service', () => { 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/'); + it('encodes the path and ref', async () => { + const encodedRef = encodeURIComponent(refWithSpecialCharMock); + const encodedUrl = `/some-project/-/refs/${encodedRef}/logs_tree/with%20%24peci%40l%20ch%40rs%2F`; + + await requestCommits(1, 'some-project', 'with $peci@l ch@rs/', refWithSpecialCharMock); - const encodedUrl = '/some-project/-/refs/main/logs_tree/with%20%24peci%40l%20ch%40rs%2F'; expect(axios.get).toHaveBeenCalledWith(encodedUrl, expect.anything()); }); @@ -68,7 +71,7 @@ describe('commits service', () => { it('calls `createAlert` when the request fails', async () => { const invalidPath = '/#@ some/path'; const invalidUrl = `${url}${invalidPath}`; - mock.onGet(invalidUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, [], {}); + mock.onGet(invalidUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, [], {}); await requestCommits(1, 'my-project', invalidPath); diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js index 6ece72c41bb..2e8860f67ef 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -25,7 +25,7 @@ import CodeIntelligence from '~/code_navigation/components/app.vue'; import * as urlUtility from '~/lib/utils/url_utility'; import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import httpStatusCodes from '~/lib/utils/http_status'; +import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import LineHighlighter from '~/blob/line_highlighter'; import { LEGACY_FILE_TYPES } from '~/repository/constants'; import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants'; @@ -256,19 +256,19 @@ describe('Blob content viewer component', () => { ); it('loads the LineHighlighter', async () => { - mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test'); + mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, 'test'); await createComponent({ blob: { ...simpleViewerMock, fileType, highlightJs } }); expect(LineHighlighter).toHaveBeenCalled(); }); it('does not load the LineHighlighter for RichViewers', async () => { - mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test'); + mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, 'test'); await createComponent({ blob: { ...richViewerMock, fileType, highlightJs } }); expect(LineHighlighter).not.toHaveBeenCalled(); }); it('scrolls to the hash', async () => { - mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test'); + mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, 'test'); await createComponent({ blob: { ...simpleViewerMock, fileType, highlightJs } }); expect(handleLocationHash).toHaveBeenCalled(); }); @@ -368,7 +368,7 @@ describe('Blob content viewer component', () => { it('does not load a CodeIntelligence component when no viewers are loaded', async () => { const url = 'some_file.js?format=json&viewer=rich'; - mockAxios.onGet(url).replyOnce(httpStatusCodes.INTERNAL_SERVER_ERROR); + mockAxios.onGet(url).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); await createComponent({ blob: { ...richViewerMock, fileType: 'unknown' } }); expect(findCodeIntelligence().exists()).toBe(false); diff --git a/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js new file mode 100644 index 00000000000..51f3d31ec72 --- /dev/null +++ b/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js @@ -0,0 +1,40 @@ +import { GlLoadingIcon } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import NotebookViewer from '~/repository/components/blob_viewers/notebook_viewer.vue'; +import notebookLoader from '~/blob/notebook'; + +jest.mock('~/blob/notebook'); + +describe('Notebook Viewer', () => { + let wrapper; + + const ROOT_RELATIVE_PATH = '/some/notebook/'; + const DEFAULT_BLOB_DATA = { rawPath: `${ROOT_RELATIVE_PATH}file.ipynb` }; + + const createComponent = () => { + wrapper = shallowMountExtended(NotebookViewer, { + propsData: { blob: DEFAULT_BLOB_DATA }, + }); + }; + + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findNotebookWrapper = () => wrapper.findByTestId('notebook'); + + beforeEach(() => createComponent()); + + it('calls the notebook loader', () => { + expect(notebookLoader).toHaveBeenCalledWith({ + el: wrapper.vm.$refs.viewer, + relativeRawPath: ROOT_RELATIVE_PATH, + }); + }); + + it('renders a loading icon component', () => { + expect(findLoadingIcon().props('size')).toBe('lg'); + }); + + it('renders the notebook wrapper', () => { + expect(findNotebookWrapper().exists()).toBe(true); + expect(findNotebookWrapper().attributes('data-endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath); + }); +}); diff --git a/spec/frontend/repository/components/blob_viewers/openapi_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/openapi_viewer_spec.js new file mode 100644 index 00000000000..21994d04076 --- /dev/null +++ b/spec/frontend/repository/components/blob_viewers/openapi_viewer_spec.js @@ -0,0 +1,30 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import OpenapiViewer from '~/repository/components/blob_viewers/openapi_viewer.vue'; +import renderOpenApi from '~/blob/openapi'; + +jest.mock('~/blob/openapi'); + +describe('OpenAPI Viewer', () => { + let wrapper; + + const DEFAULT_BLOB_DATA = { rawPath: 'some/openapi.yml' }; + + const createOpenApiViewer = () => { + wrapper = shallowMountExtended(OpenapiViewer, { + propsData: { blob: DEFAULT_BLOB_DATA }, + }); + }; + + const findOpenApiViewer = () => wrapper.findByTestId('openapi'); + + beforeEach(() => createOpenApiViewer()); + + it('calls the openapi render', () => { + expect(renderOpenApi).toHaveBeenCalledWith(wrapper.vm.$refs.viewer); + }); + + it('renders an openapi viewer', () => { + expect(findOpenApiViewer().exists()).toBe(true); + expect(findOpenApiViewer().attributes('data-endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath); + }); +}); diff --git a/spec/frontend/repository/components/fork_info_spec.js b/spec/frontend/repository/components/fork_info_spec.js new file mode 100644 index 00000000000..c23d5ae5823 --- /dev/null +++ b/spec/frontend/repository/components/fork_info_spec.js @@ -0,0 +1,122 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlSkeletonLoader, GlIcon, GlLink } from '@gitlab/ui'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { createAlert } from '~/flash'; + +import ForkInfo, { i18n } from '~/repository/components/fork_info.vue'; +import forkDetailsQuery from '~/repository/queries/fork_details.query.graphql'; +import { propsForkInfo } from '../mock_data'; + +jest.mock('~/flash'); + +describe('ForkInfo component', () => { + let wrapper; + let mockResolver; + const forkInfoError = new Error('Something went wrong'); + + Vue.use(VueApollo); + + const createCommitData = ({ ahead = 3, behind = 7 }) => { + return { + data: { + project: { id: '1', forkDetails: { ahead, behind, __typename: 'ForkDetails' } }, + }, + }; + }; + + const createComponent = (props = {}, data = {}, isRequestFailed = false) => { + mockResolver = isRequestFailed + ? jest.fn().mockRejectedValue(forkInfoError) + : jest.fn().mockResolvedValue(createCommitData(data)); + + wrapper = shallowMountExtended(ForkInfo, { + apolloProvider: createMockApollo([[forkDetailsQuery, mockResolver]]), + propsData: { ...propsForkInfo, ...props }, + }); + return waitForPromises(); + }; + + const findLink = () => wrapper.findComponent(GlLink); + const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader); + const findIcon = () => wrapper.findComponent(GlIcon); + const findDivergenceMessage = () => wrapper.find('.gl-text-secondary'); + const findInaccessibleMessage = () => wrapper.findByTestId('inaccessible-project'); + it('displays a skeleton while loading data', async () => { + createComponent(); + expect(findSkeleton().exists()).toBe(true); + }); + + it('does not display skeleton when data is loaded', async () => { + await createComponent(); + expect(findSkeleton().exists()).toBe(false); + }); + + it('renders fork icon', async () => { + await createComponent(); + expect(findIcon().exists()).toBe(true); + }); + + it('queries the data when sourceName is present', async () => { + await createComponent(); + expect(mockResolver).toHaveBeenCalled(); + }); + + it('does not query the data when sourceName is empty', async () => { + await createComponent({ sourceName: null }); + expect(mockResolver).not.toHaveBeenCalled(); + }); + + it('renders inaccessible message when fork source is not available', async () => { + await createComponent({ sourceName: '' }); + const message = findInaccessibleMessage(); + expect(message.exists()).toBe(true); + expect(message.text()).toBe(i18n.inaccessibleProject); + }); + + it('shows source project name with a link to a repo', async () => { + await createComponent(); + const link = findLink(); + expect(link.text()).toBe(propsForkInfo.sourceName); + expect(link.attributes('href')).toBe(propsForkInfo.sourcePath); + }); + + it('renders unknown divergence message when divergence is unknown', async () => { + await createComponent({}, { ahead: null, behind: null }); + expect(findDivergenceMessage().text()).toBe(i18n.unknown); + }); + + it('shows correct divergence message when data is present', async () => { + await createComponent(); + expect(findDivergenceMessage().text()).toMatchInterpolatedText( + '7 commits behind, 3 commits ahead of the upstream repository.', + ); + }); + + it('renders up to date message when divergence is unknown', async () => { + await createComponent({}, { ahead: 0, behind: 0 }); + expect(findDivergenceMessage().text()).toBe(i18n.upToDate); + }); + + it('renders commits ahead message', async () => { + await createComponent({}, { behind: 0 }); + expect(findDivergenceMessage().text()).toBe('3 commits ahead of the upstream repository.'); + }); + + it('renders commits behind message', async () => { + await createComponent({}, { ahead: 0 }); + + expect(findDivergenceMessage().text()).toBe('7 commits behind the upstream repository.'); + }); + + it('renders alert with error message when request fails', async () => { + await createComponent({}, {}, true); + expect(createAlert).toHaveBeenCalledWith({ + message: i18n.error, + captureError: true, + error: forkInfoError, + }); + }); +}); diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js index cf0d48280f4..4e5c9a685c4 100644 --- a/spec/frontend/repository/components/new_directory_modal_spec.js +++ b/spec/frontend/repository/components/new_directory_modal_spec.js @@ -5,7 +5,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/flash'; -import httpStatusCodes from '~/lib/utils/http_status'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; import NewDirectoryModal from '~/repository/components/new_directory_modal.vue'; @@ -149,7 +149,7 @@ describe('NewDirectoryModal', () => { originalBranch, createNewMr, } = defaultFormValue; - mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {}); + mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, {}); await fillForm(); await submitForm(); @@ -161,7 +161,7 @@ describe('NewDirectoryModal', () => { }); it('does not submit "create_merge_request" formData if createNewMr is not checked', async () => { - mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {}); + mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, {}); await fillForm({ createNewMr: false }); await submitForm(); expect(mock.history.post[0].data.get('create_merge_request')).toBeNull(); @@ -169,7 +169,7 @@ describe('NewDirectoryModal', () => { it('redirects to the new directory', async () => { const response = { filePath: 'new-dir-path' }; - mock.onPost(initialProps.path).reply(httpStatusCodes.OK, response); + mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, response); await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' }); await submitForm(); diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js index 6eea66f1a7d..f694c8e9166 100644 --- a/spec/frontend/repository/components/tree_content_spec.js +++ b/spec/frontend/repository/components/tree_content_spec.js @@ -5,19 +5,25 @@ import FilePreview from '~/repository/components/preview/index.vue'; import FileTable from '~/repository/components/table/index.vue'; import TreeContent from 'jh_else_ce/repository/components/tree_content.vue'; import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; +import { i18n } from '~/repository/constants'; +import { graphQLErrors } from '../mock_data'; jest.mock('~/repository/commits_service', () => ({ loadCommits: jest.fn(() => Promise.resolve()), isRequested: jest.fn(), resetRequestedCommits: jest.fn(), })); +jest.mock('~/flash'); let vm; let $apollo; +const mockResponse = jest.fn().mockReturnValue(Promise.resolve({ data: {} })); -function factory(path, data = () => ({})) { +function factory(path, appoloMockResponse = mockResponse) { $apollo = { - query: jest.fn().mockReturnValue(Promise.resolve({ data: data() })), + query: appoloMockResponse, }; vm = shallowMount(TreeContent, { @@ -222,4 +228,17 @@ describe('Repository table component', () => { expect(loadCommits.mock.calls).toEqual([['', path, '', 0]]); }); }); + + describe('error handling', () => { + const gitalyError = { graphQLErrors }; + it.each` + error | message + ${gitalyError} | ${i18n.gitalyError} + ${'Error'} | ${i18n.generalError} + `('should show an expected error', async ({ error, message }) => { + factory('/', jest.fn().mockRejectedValue(error)); + await waitForPromises(); + expect(createAlert).toHaveBeenCalledWith({ message, captureError: true }); + }); + }); }); diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js index 8db169b02b4..9de0666f27a 100644 --- a/spec/frontend/repository/components/upload_blob_modal_spec.js +++ b/spec/frontend/repository/components/upload_blob_modal_spec.js @@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/flash'; -import httpStatusCodes from '~/lib/utils/http_status'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; @@ -158,7 +158,7 @@ describe('UploadBlobModal', () => { describe('successful response', () => { beforeEach(async () => { mock = new MockAdapter(axios); - mock.onPost(initialProps.path).reply(httpStatusCodes.OK, { filePath: 'blah' }); + mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, { filePath: 'blah' }); findModal().vm.$emit('primary', mockEvent); diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js index cda47a5b0a5..d85434a9148 100644 --- a/spec/frontend/repository/mock_data.js +++ b/spec/frontend/repository/mock_data.js @@ -87,6 +87,8 @@ export const applicationInfoMock = { gitpodEnabled: true }; export const propsMock = { path: 'some_file.js', projectPath: 'some/path' }; export const refMock = 'default-ref'; +export const refWithSpecialCharMock = 'feat/selected-#-ref-#'; +export const encodedRefWithSpecialCharMock = 'feat/selected-%23-ref-%23'; export const blobControlsDataMock = { id: '1234', @@ -106,3 +108,19 @@ export const blobControlsDataMock = { }, }, }; + +export const graphQLErrors = [ + { + message: '14:failed to connect to all addresses.', + locations: [{ line: 16, column: 7 }], + path: ['project', 'repository', 'paginatedTree'], + extensions: { code: 'unavailable', gitaly_code: 14, service: 'git' }, + }, +]; + +export const propsForkInfo = { + projectPath: 'nataliia/myGitLab', + selectedRef: 'main', + sourceName: 'gitLab', + sourcePath: 'gitlab-org/gitlab', +}; diff --git a/spec/frontend/repository/utils/ref_switcher_utils_spec.js b/spec/frontend/repository/utils/ref_switcher_utils_spec.js index 3335059554f..4d0250fffbf 100644 --- a/spec/frontend/repository/utils/ref_switcher_utils_spec.js +++ b/spec/frontend/repository/utils/ref_switcher_utils_spec.js @@ -1,5 +1,6 @@ import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils'; import setWindowLocation from 'helpers/set_window_location_helper'; +import { refWithSpecialCharMock, encodedRefWithSpecialCharMock } from '../mock_data'; const projectRootPath = 'root/Project1'; const currentRef = 'main'; @@ -19,4 +20,10 @@ describe('generateRefDestinationPath', () => { setWindowLocation(currentPath); expect(generateRefDestinationPath(projectRootPath, selectedRef)).toBe(result); }); + + it('encodes the selected ref', () => { + const result = `${projectRootPath}/-/tree/${encodedRefWithSpecialCharMock}`; + + expect(generateRefDestinationPath(projectRootPath, refWithSpecialCharMock)).toBe(result); + }); }); |