diff options
Diffstat (limited to 'spec/frontend/repository/components')
12 files changed, 591 insertions, 93 deletions
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js new file mode 100644 index 00000000000..a449fd6f06c --- /dev/null +++ b/spec/frontend/repository/components/blob_button_group_spec.js @@ -0,0 +1,117 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import BlobButtonGroup from '~/repository/components/blob_button_group.vue'; +import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue'; +import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; + +const DEFAULT_PROPS = { + name: 'some name', + path: 'some/path', + canPushCode: true, + replacePath: 'some/replace/path', + deletePath: 'some/delete/path', + emptyRepo: false, +}; + +const DEFAULT_INJECT = { + targetBranch: 'master', + originalBranch: 'master', +}; + +describe('BlobButtonGroup component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(BlobButtonGroup, { + propsData: { + ...DEFAULT_PROPS, + ...props, + }, + provide: { + ...DEFAULT_INJECT, + }, + directives: { + GlModal: createMockDirective(), + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal); + const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); + const findReplaceButton = () => wrapper.findAll(GlButton).at(0); + + it('renders component', () => { + createComponent(); + + const { name, path } = DEFAULT_PROPS; + + expect(wrapper.props()).toMatchObject({ + name, + path, + }); + }); + + describe('buttons', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders both the replace and delete button', () => { + expect(wrapper.findAll(GlButton)).toHaveLength(2); + }); + + it('renders the buttons in the correct order', () => { + expect(wrapper.findAll(GlButton).at(0).text()).toBe('Replace'); + expect(wrapper.findAll(GlButton).at(1).text()).toBe('Delete'); + }); + + it('triggers the UploadBlobModal from the replace button', () => { + const { value } = getBinding(findReplaceButton().element, 'gl-modal'); + const modalId = findUploadBlobModal().props('modalId'); + + expect(modalId).toEqual(value); + }); + }); + + it('renders UploadBlobModal', () => { + createComponent(); + + const { targetBranch, originalBranch } = DEFAULT_INJECT; + const { name, path, canPushCode, replacePath } = DEFAULT_PROPS; + const title = `Replace ${name}`; + + expect(findUploadBlobModal().props()).toMatchObject({ + modalTitle: title, + commitMessage: title, + targetBranch, + originalBranch, + canPushCode, + path, + replacePath, + primaryBtnText: 'Replace file', + }); + }); + + it('renders DeleteBlobModel', () => { + createComponent(); + + const { targetBranch, originalBranch } = DEFAULT_INJECT; + const { name, canPushCode, deletePath, emptyRepo } = DEFAULT_PROPS; + const title = `Delete ${name}`; + + expect(findDeleteBlobModal().props()).toMatchObject({ + modalTitle: title, + commitMessage: title, + targetBranch, + originalBranch, + canPushCode, + deletePath, + emptyRepo, + }); + }); +}); diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js index 495039b4ccb..a83d0a607f2 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -1,11 +1,23 @@ import { GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { shallowMount, mount, createLocalVue } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import BlobContent from '~/blob/components/blob_content.vue'; 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 BlobHeaderEdit from '~/repository/components/blob_header_edit.vue'; -import BlobReplace from '~/repository/components/blob_replace.vue'; +import BlobEdit from '~/repository/components/blob_edit.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'; + +jest.mock('~/repository/components/blob_viewers'); let wrapper; const simpleMockData = { @@ -17,6 +29,7 @@ const simpleMockData = { fileType: 'text', tooLarge: false, path: 'some_file.js', + webPath: 'some_file.js', editBlobPath: 'some_file.js/edit', ideEditPath: 'some_file.js/ide/edit', storedExternally: false, @@ -27,7 +40,6 @@ const simpleMockData = { canLock: true, isLocked: false, lockLink: 'some_file.js/lock', - canModifyBlob: true, forkPath: 'some_file.js/fork', simpleViewer: { fileType: 'text', @@ -47,6 +59,51 @@ const richMockData = { }, }; +const projectMockData = { + userPermissions: { + pushCode: true, + }, + repository: { + empty: false, + }, +}; + +const localVue = createLocalVue(); +const mockAxios = new MockAdapter(axios); + +const createComponentWithApollo = (mockData = {}) => { + localVue.use(VueApollo); + + const defaultPushCode = projectMockData.userPermissions.pushCode; + const defaultEmptyRepo = projectMockData.repository.empty; + const { blobs, emptyRepo = defaultEmptyRepo, canPushCode = defaultPushCode } = mockData; + + const mockResolver = jest.fn().mockResolvedValue({ + data: { + project: { + userPermissions: { pushCode: canPushCode }, + repository: { + empty: emptyRepo, + blobs: { + nodes: [blobs], + }, + }, + }, + }, + }); + + const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]); + + wrapper = shallowMount(BlobContentViewer, { + localVue, + apolloProvider: fakeApollo, + propsData: { + path: 'some_file.js', + projectPath: 'some/path', + }, + }); +}; + const createFactory = (mountFn) => ( { props = {}, mockData = {}, stubs = {} } = {}, loading = false, @@ -78,9 +135,9 @@ const fullFactory = createFactory(mount); describe('Blob content viewer component', () => { const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findBlobHeader = () => wrapper.findComponent(BlobHeader); - const findBlobHeaderEdit = () => wrapper.findComponent(BlobHeaderEdit); + const findBlobEdit = () => wrapper.findComponent(BlobEdit); const findBlobContent = () => wrapper.findComponent(BlobContent); - const findBlobReplace = () => wrapper.findComponent(BlobReplace); + const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup); afterEach(() => { wrapper.destroy(); @@ -163,6 +220,67 @@ 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 }); + await waitForPromises(); + + expect(mockAxios.history.get).toHaveLength(1); + }); + }); + + describe('Blob viewer', () => { + afterEach(() => { + loadViewer.mockRestore(); + viewerProps.mockRestore(); + }); + + it('does not render a BlobContent component if a Blob viewer is available', () => { + loadViewer.mockReturnValueOnce(() => true); + factory({ mockData: { blobInfo: richMockData } }); + + expect(findBlobContent().exists()).toBe(false); + }); + + it.each` + viewer | loadViewerReturnValue | viewerPropsReturnValue + ${'empty'} | ${EmptyViewer} | ${{}} + ${'download'} | ${DownloadViewer} | ${{ filePath: '/some/file/path', fileName: 'test.js', fileSize: 100 }} + ${'text'} | ${TextViewer} | ${{ content: 'test', fileName: 'test.js', readOnly: true }} + `( + 'renders viewer component for $viewer files', + async ({ viewer, loadViewerReturnValue, viewerPropsReturnValue }) => { + loadViewer.mockReturnValue(loadViewerReturnValue); + viewerProps.mockReturnValue(viewerPropsReturnValue); + + factory({ + mockData: { + blobInfo: { + ...simpleMockData, + fileType: null, + simpleViewer: { + ...simpleMockData.simpleViewer, + fileType: viewer, + }, + }, + }, + }); + + await nextTick(); + + expect(loadViewer).toHaveBeenCalledWith(viewer); + expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true); + }, + ); + }); + describe('BlobHeader action slot', () => { const { ideEditPath, editBlobPath } = simpleMockData; @@ -177,7 +295,7 @@ describe('Blob content viewer component', () => { await nextTick(); - expect(findBlobHeaderEdit().props()).toMatchObject({ + expect(findBlobEdit().props()).toMatchObject({ editPath: editBlobPath, webIdePath: ideEditPath, }); @@ -194,31 +312,56 @@ describe('Blob content viewer component', () => { await nextTick(); - expect(findBlobHeaderEdit().props()).toMatchObject({ + expect(findBlobEdit().props()).toMatchObject({ editPath: editBlobPath, webIdePath: ideEditPath, }); }); - describe('BlobReplace', () => { - const { name, path } = simpleMockData; + it('does not render BlobHeaderEdit button when viewing a binary file', async () => { + fullFactory({ + mockData: { blobInfo: richMockData, isBinary: true }, + stubs: { + BlobContent: true, + BlobReplace: true, + }, + }); + + await nextTick(); + + expect(findBlobEdit().exists()).toBe(false); + }); + + describe('BlobButtonGroup', () => { + const { name, path, replacePath, webPath } = simpleMockData; + const { + userPermissions: { pushCode }, + repository: { empty }, + } = projectMockData; it('renders component', async () => { window.gon.current_user_id = 1; fullFactory({ - mockData: { blobInfo: simpleMockData }, + mockData: { + blobInfo: simpleMockData, + project: { userPermissions: { pushCode }, repository: { empty } }, + }, stubs: { BlobContent: true, - BlobReplace: true, + BlobButtonGroup: true, }, }); await nextTick(); - expect(findBlobReplace().props()).toMatchObject({ + expect(findBlobButtonGroup().props()).toMatchObject({ name, path, + replacePath, + deletePath: webPath, + canPushCode: pushCode, + emptyRepo: empty, }); }); @@ -235,7 +378,7 @@ describe('Blob content viewer component', () => { await nextTick(); - expect(findBlobReplace().exists()).toBe(false); + expect(findBlobButtonGroup().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/repository/components/blob_header_edit_spec.js b/spec/frontend/repository/components/blob_edit_spec.js index c0eb7c523c4..e6e69cd8549 100644 --- a/spec/frontend/repository/components/blob_header_edit_spec.js +++ b/spec/frontend/repository/components/blob_edit_spec.js @@ -1,6 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import BlobHeaderEdit from '~/repository/components/blob_header_edit.vue'; +import BlobEdit from '~/repository/components/blob_edit.vue'; import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; const DEFAULT_PROPS = { @@ -8,11 +8,11 @@ const DEFAULT_PROPS = { webIdePath: 'some_file.js/ide/edit', }; -describe('BlobHeaderEdit component', () => { +describe('BlobEdit component', () => { let wrapper; const createComponent = (consolidatedEditButton = false, props = {}) => { - wrapper = shallowMount(BlobHeaderEdit, { + wrapper = shallowMount(BlobEdit, { propsData: { ...DEFAULT_PROPS, ...props, diff --git a/spec/frontend/repository/components/blob_replace_spec.js b/spec/frontend/repository/components/blob_replace_spec.js deleted file mode 100644 index 4a6f147da22..00000000000 --- a/spec/frontend/repository/components/blob_replace_spec.js +++ /dev/null @@ -1,67 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import BlobReplace from '~/repository/components/blob_replace.vue'; -import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; - -const DEFAULT_PROPS = { - name: 'some name', - path: 'some/path', - canPushCode: true, - replacePath: 'some/replace/path', -}; - -const DEFAULT_INJECT = { - targetBranch: 'master', - originalBranch: 'master', -}; - -describe('BlobReplace component', () => { - let wrapper; - - const createComponent = (props = {}) => { - wrapper = shallowMount(BlobReplace, { - propsData: { - ...DEFAULT_PROPS, - ...props, - }, - provide: { - ...DEFAULT_INJECT, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); - - it('renders component', () => { - createComponent(); - - const { name, path } = DEFAULT_PROPS; - - expect(wrapper.props()).toMatchObject({ - name, - path, - }); - }); - - it('renders UploadBlobModal', () => { - createComponent(); - - const { targetBranch, originalBranch } = DEFAULT_INJECT; - const { name, path, canPushCode, replacePath } = DEFAULT_PROPS; - const title = `Replace ${name}`; - - expect(findUploadBlobModal().props()).toMatchObject({ - modalTitle: title, - commitMessage: title, - targetBranch, - originalBranch, - canPushCode, - path, - replacePath, - primaryBtnText: 'Replace file', - }); - }); -}); diff --git a/spec/frontend/repository/components/blob_viewers/__snapshots__/empty_viewer_spec.js.snap b/spec/frontend/repository/components/blob_viewers/__snapshots__/empty_viewer_spec.js.snap new file mode 100644 index 00000000000..e702ea5fd00 --- /dev/null +++ b/spec/frontend/repository/components/blob_viewers/__snapshots__/empty_viewer_spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Empty Viewer matches the snapshot 1`] = ` +<div + class="nothing-here-block" +> + Empty file +</div> +`; diff --git a/spec/frontend/repository/components/blob_viewers/download_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/download_viewer_spec.js new file mode 100644 index 00000000000..c71b2b3c55c --- /dev/null +++ b/spec/frontend/repository/components/blob_viewers/download_viewer_spec.js @@ -0,0 +1,70 @@ +import { GlLink, GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue'; + +describe('Text Viewer', () => { + let wrapper; + + const DEFAULT_PROPS = { + fileName: 'file_name.js', + filePath: '/some/file/path', + fileSize: 2269674, + }; + + const createComponent = (props = {}) => { + wrapper = shallowMount(DownloadViewer, { + propsData: { + ...DEFAULT_PROPS, + ...props, + }, + }); + }; + + it('renders component', () => { + createComponent(); + + const { fileName, filePath, fileSize } = DEFAULT_PROPS; + expect(wrapper.props()).toMatchObject({ + fileName, + filePath, + fileSize, + }); + }); + + it('renders download human readable file size text', () => { + createComponent(); + + const downloadText = `Download (${numberToHumanSize(DEFAULT_PROPS.fileSize)})`; + expect(wrapper.text()).toBe(downloadText); + }); + + it('renders download text', () => { + createComponent({ + fileSize: 0, + }); + + expect(wrapper.text()).toBe('Download'); + }); + + it('renders download link', () => { + createComponent(); + const { filePath, fileName } = DEFAULT_PROPS; + + expect(wrapper.findComponent(GlLink).attributes()).toMatchObject({ + rel: 'nofollow', + target: '_blank', + href: filePath, + download: fileName, + }); + }); + + it('renders download icon', () => { + createComponent(); + + expect(wrapper.findComponent(GlIcon).props()).toMatchObject({ + name: 'download', + size: 16, + }); + }); +}); diff --git a/spec/frontend/repository/components/blob_viewers/empty_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/empty_viewer_spec.js new file mode 100644 index 00000000000..e65f20ea0af --- /dev/null +++ b/spec/frontend/repository/components/blob_viewers/empty_viewer_spec.js @@ -0,0 +1,14 @@ +import { shallowMount } from '@vue/test-utils'; +import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue'; + +describe('Empty Viewer', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(EmptyViewer); + }); + + it('matches the snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/repository/components/blob_viewers/text_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/text_viewer_spec.js new file mode 100644 index 00000000000..88c5bee6564 --- /dev/null +++ b/spec/frontend/repository/components/blob_viewers/text_viewer_spec.js @@ -0,0 +1,30 @@ +import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue'; +import SourceEditor from '~/vue_shared/components/source_editor.vue'; + +describe('Text Viewer', () => { + let wrapper; + const propsData = { + content: 'Some content', + fileName: 'file_name.js', + readOnly: true, + }; + + const createComponent = () => { + wrapper = shallowMount(TextViewer, { propsData }); + }; + + const findEditor = () => wrapper.findComponent(SourceEditor); + + it('renders a Source Editor component', async () => { + createComponent(); + + await waitForPromises(); + + expect(findEditor().exists()).toBe(true); + expect(findEditor().props('value')).toBe(propsData.content); + expect(findEditor().props('fileName')).toBe(propsData.fileName); + expect(findEditor().props('editorOptions')).toEqual({ readOnly: propsData.readOnly }); + }); +}); diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js new file mode 100644 index 00000000000..a74e3e6d325 --- /dev/null +++ b/spec/frontend/repository/components/delete_blob_modal_spec.js @@ -0,0 +1,130 @@ +import { GlFormTextarea, GlModal, GlFormInput, GlToggle } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue'; + +jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); + +const initialProps = { + modalId: 'Delete-blob', + modalTitle: 'Delete File', + deletePath: 'some/path', + commitMessage: 'Delete File', + targetBranch: 'some-target-branch', + originalBranch: 'main', + canPushCode: true, + emptyRepo: false, +}; + +describe('DeleteBlobModal', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(DeleteBlobModal, { + propsData: { + ...initialProps, + ...props, + }, + }); + }; + + const findModal = () => wrapper.findComponent(GlModal); + const findForm = () => wrapper.findComponent({ ref: 'form' }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders Modal component', () => { + createComponent(); + + const { modalTitle: title } = initialProps; + + expect(findModal().props()).toMatchObject({ + title, + size: 'md', + actionPrimary: { + text: 'Delete file', + }, + actionCancel: { + text: 'Cancel', + }, + }); + }); + + describe('form', () => { + it('gets passed the path for action attribute', () => { + createComponent(); + expect(findForm().attributes('action')).toBe(initialProps.deletePath); + }); + + it('submits the form', async () => { + createComponent(); + + const submitSpy = jest.spyOn(findForm().element, 'submit'); + findModal().vm.$emit('primary', { preventDefault: () => {} }); + await nextTick(); + + expect(submitSpy).toHaveBeenCalled(); + submitSpy.mockRestore(); + }); + + it.each` + component | defaultValue | canPushCode | targetBranch | originalBranch | exist + ${GlFormTextarea} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${GlFormInput} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${GlFormInput} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false} + ${GlToggle} | ${'true'} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${GlToggle} | ${undefined} | ${true} | ${'same-branch'} | ${'same-branch'} | ${false} + `( + 'has the correct form fields ', + ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => { + createComponent({ + canPushCode, + targetBranch, + originalBranch, + }); + const formField = wrapper.findComponent(component); + + if (!exist) { + expect(formField.exists()).toBe(false); + return; + } + + expect(formField.exists()).toBe(true); + expect(formField.attributes('value')).toBe(defaultValue); + }, + ); + + it.each` + input | value | emptyRepo | canPushCode | exist + ${'authenticity_token'} | ${'mock-csrf-token'} | ${false} | ${true} | ${true} + ${'authenticity_token'} | ${'mock-csrf-token'} | ${true} | ${false} | ${true} + ${'_method'} | ${'delete'} | ${false} | ${true} | ${true} + ${'_method'} | ${'delete'} | ${true} | ${false} | ${true} + ${'original_branch'} | ${initialProps.originalBranch} | ${false} | ${true} | ${true} + ${'original_branch'} | ${undefined} | ${true} | ${true} | ${false} + ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true} + ${'create_merge_request'} | ${'1'} | ${false} | ${true} | ${true} + ${'create_merge_request'} | ${undefined} | ${true} | ${false} | ${false} + `( + 'passes $input as a hidden input with the correct value', + ({ input, value, emptyRepo, canPushCode, exist }) => { + createComponent({ + emptyRepo, + canPushCode, + }); + + const inputMethod = findForm().find(`input[name="${input}"]`); + + if (!exist) { + expect(inputMethod.exists()).toBe(false); + return; + } + + expect(inputMethod.attributes('type')).toBe('hidden'); + expect(inputMethod.attributes('value')).toBe(value); + }, + ); + }); +}); 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 ac60fc4917d..6f461f4c69b 100644 --- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap +++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap @@ -11,6 +11,7 @@ exports[`Repository table row component renders a symlink table row 1`] = ` class="tree-item-link str-truncated" data-qa-selector="file_name_link" href="https://test.com" + title="test" > <file-icon-stub class="mr-1 position-relative text-secondary" @@ -64,6 +65,7 @@ exports[`Repository table row component renders table row 1`] = ` class="tree-item-link str-truncated" data-qa-selector="file_name_link" href="https://test.com" + title="test" > <file-icon-stub class="mr-1 position-relative text-secondary" @@ -117,6 +119,7 @@ exports[`Repository table row component renders table row for path with special class="tree-item-link str-truncated" data-qa-selector="file_name_link" href="https://test.com" + title="test" > <file-icon-stub class="mr-1 position-relative text-secondary" diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js index d397bc185e2..1d1ec58100f 100644 --- a/spec/frontend/repository/components/tree_content_spec.js +++ b/spec/frontend/repository/components/tree_content_spec.js @@ -1,8 +1,8 @@ import { shallowMount } from '@vue/test-utils'; +import filesQuery from 'shared_queries/repository/files.query.graphql'; 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 { TREE_INITIAL_FETCH_COUNT } from '~/repository/constants'; let vm; let $apollo; @@ -19,10 +19,17 @@ function factory(path, data = () => ({})) { mocks: { $apollo, }, + provide: { + glFeatures: { + increasePageSizeExponentially: true, + }, + }, }); } describe('Repository table component', () => { + const findFileTable = () => vm.find(FileTable); + afterEach(() => { vm.destroy(); }); @@ -85,14 +92,12 @@ describe('Repository table component', () => { describe('FileTable showMore', () => { describe('when is present', () => { - const fileTable = () => vm.find(FileTable); - beforeEach(async () => { factory('/'); }); it('is changes hasShowMore to false when "showMore" event is emitted', async () => { - fileTable().vm.$emit('showMore'); + findFileTable().vm.$emit('showMore'); await vm.vm.$nextTick(); @@ -100,7 +105,7 @@ describe('Repository table component', () => { }); it('changes clickedShowMore when "showMore" event is emitted', async () => { - fileTable().vm.$emit('showMore'); + findFileTable().vm.$emit('showMore'); await vm.vm.$nextTick(); @@ -110,7 +115,7 @@ describe('Repository table component', () => { it('triggers fetchFiles when "showMore" event is emitted', () => { jest.spyOn(vm.vm, 'fetchFiles'); - fileTable().vm.$emit('showMore'); + findFileTable().vm.$emit('showMore'); expect(vm.vm.fetchFiles).toHaveBeenCalled(); }); @@ -126,10 +131,52 @@ describe('Repository table component', () => { expect(vm.vm.hasShowMore).toBe(false); }); - it('has limit of 1000 files on initial load', () => { + it.each` + totalBlobs | pagesLoaded | limitReached + ${900} | ${1} | ${false} + ${1000} | ${1} | ${true} + ${1002} | ${1} | ${true} + ${1002} | ${2} | ${false} + ${1900} | ${2} | ${false} + ${2000} | ${2} | ${true} + `('has limit of 1000 entries per page', async ({ totalBlobs, pagesLoaded, limitReached }) => { factory('/'); - expect(TREE_INITIAL_FETCH_COUNT * vm.vm.pageSize).toBe(1000); + const blobs = new Array(totalBlobs).fill('fakeBlob'); + vm.setData({ entries: { blobs }, pagesLoaded }); + + await vm.vm.$nextTick(); + + expect(findFileTable().props('hasMore')).toBe(limitReached); + }); + + it.each` + fetchCounter | pageSize + ${0} | ${10} + ${2} | ${30} + ${4} | ${50} + ${6} | ${70} + ${8} | ${90} + ${10} | ${100} + ${20} | ${100} + ${100} | ${100} + ${200} | ${100} + `('exponentially increases page size, to a maximum of 100', ({ fetchCounter, pageSize }) => { + factory('/'); + vm.setData({ fetchCounter }); + + vm.vm.fetchFiles(); + + expect($apollo.query).toHaveBeenCalledWith({ + query: filesQuery, + variables: { + pageSize, + nextPageCursor: '', + path: '/', + projectPath: '', + ref: '', + }, + }); }); }); }); diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js index d93b1d7e5f1..08a6583b60c 100644 --- a/spec/frontend/repository/components/upload_blob_modal_spec.js +++ b/spec/frontend/repository/components/upload_blob_modal_spec.js @@ -190,7 +190,9 @@ describe('UploadBlobModal', () => { }); it('creates a flash error', () => { - expect(createFlash).toHaveBeenCalledWith('Error uploading file. Please try again.'); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Error uploading file. Please try again.', + }); }); afterEach(() => { |