import { GlLoadingIcon } from '@gitlab/ui'; 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 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 = { name: 'some_file.js', size: 123, rawSize: 123, rawTextBlob: 'raw content', type: 'text', 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, rawPath: 'some_file.js', externalStorageUrl: 'some_file.js', replacePath: 'some_file.js/replace', deletePath: 'some_file.js/delete', canLock: true, isLocked: false, lockLink: 'some_file.js/lock', forkPath: 'some_file.js/fork', simpleViewer: { fileType: 'text', tooLarge: false, type: 'simple', renderError: null, }, richViewer: null, }; const richMockData = { ...simpleMockData, richViewer: { fileType: 'markup', tooLarge: false, type: 'rich', renderError: null, }, }; 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, ) => { wrapper = mountFn(BlobContentViewer, { propsData: { path: 'some_file.js', projectPath: 'some/path', ...props, }, mocks: { $apollo: { queries: { project: { loading, }, }, }, }, stubs, }); wrapper.setData(mockData); }; const factory = createFactory(shallowMount); const fullFactory = createFactory(mount); describe('Blob content viewer component', () => { const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findBlobHeader = () => wrapper.findComponent(BlobHeader); const findBlobEdit = () => wrapper.findComponent(BlobEdit); const findBlobContent = () => wrapper.findComponent(BlobContent); const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup); afterEach(() => { wrapper.destroy(); }); it('renders a GlLoadingIcon component', () => { factory({ mockData: { blobInfo: simpleMockData } }, true); expect(findLoadingIcon().exists()).toBe(true); }); describe('simple viewer', () => { beforeEach(() => { factory({ mockData: { blobInfo: simpleMockData } }); }); it('renders a BlobHeader component', () => { expect(findBlobHeader().props('activeViewerType')).toEqual('simple'); expect(findBlobHeader().props('hasRenderError')).toEqual(false); expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true); expect(findBlobHeader().props('blob')).toEqual(simpleMockData); }); 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', tooLarge: false, type: 'simple', renderError: null, }); }); }); describe('rich viewer', () => { beforeEach(() => { factory({ mockData: { blobInfo: richMockData, activeViewerType: 'rich' }, }); }); it('renders a BlobHeader component', () => { expect(findBlobHeader().props('activeViewerType')).toEqual('rich'); expect(findBlobHeader().props('hasRenderError')).toEqual(false); expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false); expect(findBlobHeader().props('blob')).toEqual(richMockData); }); 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', tooLarge: false, type: 'rich', renderError: null, }); }); it('updates viewer type when viewer changed is clicked', async () => { expect(findBlobContent().props('activeViewer')).toEqual( expect.objectContaining({ type: 'rich', }), ); expect(findBlobHeader().props('activeViewerType')).toEqual('rich'); findBlobHeader().vm.$emit('viewer-changed', 'simple'); await nextTick(); expect(findBlobHeader().props('activeViewerType')).toEqual('simple'); expect(findBlobContent().props('activeViewer')).toEqual( expect.objectContaining({ type: 'simple', }), ); }); }); 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; it('renders BlobHeaderEdit buttons in simple viewer', async () => { fullFactory({ mockData: { blobInfo: simpleMockData }, stubs: { BlobContent: true, BlobReplace: true, }, }); await nextTick(); expect(findBlobEdit().props()).toMatchObject({ editPath: editBlobPath, webIdePath: ideEditPath, }); }); it('renders BlobHeaderEdit button in rich viewer', async () => { fullFactory({ mockData: { blobInfo: richMockData }, stubs: { BlobContent: true, BlobReplace: true, }, }); await nextTick(); expect(findBlobEdit().props()).toMatchObject({ editPath: editBlobPath, webIdePath: ideEditPath, }); }); 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, project: { userPermissions: { pushCode }, repository: { empty } }, }, stubs: { BlobContent: true, BlobButtonGroup: true, }, }); await nextTick(); expect(findBlobButtonGroup().props()).toMatchObject({ name, path, replacePath, deletePath: webPath, canPushCode: pushCode, emptyRepo: empty, }); }); it('does not render if not logged in', async () => { window.gon.current_user_id = null; fullFactory({ mockData: { blobInfo: simpleMockData }, stubs: { BlobContent: true, BlobReplace: true, }, }); await nextTick(); expect(findBlobButtonGroup().exists()).toBe(false); }); }); }); });