// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {render, fireEvent, act} from '@testing-library/react-native'; import React, {useMemo, type ComponentProps} from 'react'; import {DeviceEventEmitter, Text, TouchableOpacity, View} from 'react-native'; import {Events} from '@constants'; import {useIsTablet} from '@hooks/device'; import {useImageAttachments} from '@hooks/files'; import TestHelper from '@test/test_helper'; import {isImage, isVideo} from '@utils/file'; import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery'; import {getViewPortWidth} from '@utils/images'; import File from './file'; import Files from './files'; import type FileModel from '@typings/database/models/servers/file'; jest.mock('@hooks/device', () => ({ useIsTablet: jest.fn().mockReturnValue(false), })); jest.mock('@hooks/files', () => ({ useImageAttachments: jest.fn().mockReturnValue({ images: [], nonImages: [], }), })); jest.mock('@hooks/utils', () => ({ usePreventDoubleTap: (fn: () => void) => fn, })); jest.mock('@utils/file', () => ({ isImage: jest.fn().mockReturnValue(true), isVideo: jest.fn().mockReturnValue(false), })); jest.mock('@utils/gallery', () => ({ fileToGalleryItem: jest.fn(), openGalleryAtIndex: jest.fn(), })); jest.mock('@utils/images', () => ({ getViewPortWidth: jest.fn().mockReturnValue(300), })); jest.mock('./file', () => ({ __esModule: true, default: jest.fn(), })); jest.mocked(File).mockImplementation((props) => ( {props.galleryIdentifier} {String(props.canDownloadFiles)} {props.index} {String(props.isSingleImage)} {String(props.nonVisibleImagesCount)} {props.wrapperWidth} {String(props.inViewPort)} {String(props.enableSecureFilePreview)} props.onPress(props.index)} /> props.updateFileForGallery(props.index, {...props.file, uri: 'updated'})} /> )); function getBaseProps(): ComponentProps { return { canDownloadFiles: true, enableSecureFilePreview: false, failed: false, filesInfo: [], isReplyPost: false, layoutWidth: 300, location: 'test-location', postId: 'test-post-id', postProps: {}, }; } describe('Files', () => { it('should render attachments, with images in the image row', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '1'}), TestHelper.fakeFileInfo({id: '2'}), TestHelper.fakeFileInfo({id: '3'}), TestHelper.fakeFileInfo({id: '4'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1' || f.id === '2'), nonImages: fi.filter((f) => f.id === '3' || f.id === '4'), }), [fi]); }); const {getByTestId} = render( , ); expect(getByTestId('1')).toBeVisible(); expect(getByTestId('2')).toBeVisible(); expect(getByTestId('3')).toBeVisible(); expect(getByTestId('4')).toBeVisible(); expect(getByTestId('image-row')).toContainElement(getByTestId('1')); expect(getByTestId('image-row')).toContainElement(getByTestId('2')); expect(getByTestId('image-row')).not.toContainElement(getByTestId('3')); expect(getByTestId('image-row')).not.toContainElement(getByTestId('4')); }); it('should not show the image row if no images', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '3'}), TestHelper.fakeFileInfo({id: '4'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1' || f.id === '2'), nonImages: fi.filter((f) => f.id === '3' || f.id === '4'), }), [fi]); }); const {getByTestId, queryByTestId} = render( , ); expect(getByTestId('3')).toBeVisible(); expect(getByTestId('4')).toBeVisible(); expect(queryByTestId('image-row')).not.toBeVisible(); }); it('should have different opacity if failed', () => { const baseProps = getBaseProps(); baseProps.failed = true; const {getByTestId, rerender} = render( , ); expect(getByTestId('files-container')).toHaveStyle({opacity: 0.5}); baseProps.failed = false; rerender(); expect(getByTestId('files-container')).not.toHaveStyle({opacity: 0.5}); }); it('should drill all relevant props', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '1'}), TestHelper.fakeFileInfo({id: '2'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1'), nonImages: fi.filter((f) => f.id === '2'), }), [fi]); }); const baseProps = getBaseProps(); baseProps.filesInfo = filesInfo; baseProps.canDownloadFiles = false; baseProps.enableSecureFilePreview = false; const {getByTestId, rerender} = render(); expect(getByTestId('1-canDownloadFiles')).toHaveTextContent('false'); expect(getByTestId('1-enableSecureFilePreview')).toHaveTextContent('false'); expect(getByTestId('2-canDownloadFiles')).toHaveTextContent('false'); expect(getByTestId('2-enableSecureFilePreview')).toHaveTextContent('false'); baseProps.canDownloadFiles = true; baseProps.enableSecureFilePreview = true; rerender(); expect(getByTestId('1-canDownloadFiles')).toHaveTextContent('true'); expect(getByTestId('1-enableSecureFilePreview')).toHaveTextContent('true'); expect(getByTestId('2-canDownloadFiles')).toHaveTextContent('true'); expect(getByTestId('2-enableSecureFilePreview')).toHaveTextContent('true'); baseProps.canDownloadFiles = true; baseProps.enableSecureFilePreview = true; rerender(); expect(getByTestId('1-canDownloadFiles')).toHaveTextContent('true'); expect(getByTestId('1-enableSecureFilePreview')).toHaveTextContent('true'); expect(getByTestId('2-canDownloadFiles')).toHaveTextContent('true'); expect(getByTestId('2-enableSecureFilePreview')).toHaveTextContent('true'); baseProps.canDownloadFiles = false; baseProps.enableSecureFilePreview = false; rerender(); expect(getByTestId('1-canDownloadFiles')).toHaveTextContent('false'); expect(getByTestId('1-enableSecureFilePreview')).toHaveTextContent('false'); expect(getByTestId('2-canDownloadFiles')).toHaveTextContent('false'); expect(getByTestId('2-enableSecureFilePreview')).toHaveTextContent('false'); }); it('should set layoutWidth if provided', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '1'}), TestHelper.fakeFileInfo({id: '2'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1'), nonImages: fi.filter((f) => f.id === '2'), }), [fi]); }); const baseProps = getBaseProps(); baseProps.filesInfo = filesInfo; baseProps.layoutWidth = 400; const {getByTestId, rerender} = render(); expect(getByTestId('1-wrapperWidth')).toHaveTextContent('400'); expect(getByTestId('2-wrapperWidth')).toHaveTextContent('400'); expect(getByTestId('image-row')).toHaveStyle({width: 400}); baseProps.layoutWidth = 500; rerender(); expect(getByTestId('1-wrapperWidth')).toHaveTextContent('500'); expect(getByTestId('2-wrapperWidth')).toHaveTextContent('500'); expect(getByTestId('image-row')).toHaveStyle({width: 500}); }); it('should use ((getViewportWidth result) - 6) if layoutWidth is not provided', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '1'}), TestHelper.fakeFileInfo({id: '2'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1'), nonImages: fi.filter((f) => f.id === '2'), }), [fi]); }); jest.mocked(getViewPortWidth).mockReturnValue(300); jest.mocked(useIsTablet).mockReturnValue(false); const baseProps = getBaseProps(); baseProps.filesInfo = filesInfo; baseProps.layoutWidth = undefined; baseProps.isReplyPost = false; const {getByTestId, rerender} = render(); expect(getByTestId('1-wrapperWidth')).toHaveTextContent('294'); expect(getByTestId('2-wrapperWidth')).toHaveTextContent('294'); expect(getByTestId('image-row')).toHaveStyle({width: 294}); expect(getViewPortWidth).toHaveBeenCalledWith(false, false); jest.mocked(getViewPortWidth).mockReturnValue(400); jest.mocked(useIsTablet).mockReturnValue(true); baseProps.isReplyPost = true; rerender(); expect(getByTestId('1-wrapperWidth')).toHaveTextContent('394'); expect(getByTestId('2-wrapperWidth')).toHaveTextContent('394'); expect(getByTestId('image-row')).toHaveStyle({width: 394}); expect(getViewPortWidth).toHaveBeenCalledWith(true, true); }); it('calling onPress on the child should open gallery', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '1'}), TestHelper.fakeFileInfo({id: '2'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1'), nonImages: fi.filter((f) => f.id === '2'), }), [fi]); }); jest.mocked(fileToGalleryItem).mockImplementation((file) => ({ height: 100, id: file.id || '', lastPictureUpdate: 0, mime_type: file.mime_type, name: file.name, type: 'image', uri: file.uri || '', width: file.width, cacheKey: file.id || '', })); const {getByTestId} = render( , ); fireEvent.press(getByTestId('1-onPress')); expect(openGalleryAtIndex).toHaveBeenCalledWith( 'test-post-id-fileAttachments-test-location', 0, expect.arrayContaining([expect.objectContaining({id: '1'}), expect.objectContaining({id: '2'})]), ); fireEvent.press(getByTestId('2-onPress')); expect(openGalleryAtIndex).toHaveBeenCalledWith( 'test-post-id-fileAttachments-test-location', 1, expect.arrayContaining([expect.objectContaining({id: '1'}), expect.objectContaining({id: '2'})]), ); }); it('calling updateFileForGallery updates the file', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '1', uri: 'original'}), TestHelper.fakeFileInfo({id: '2', uri: 'original'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1'), nonImages: fi.filter((f) => f.id === '2'), }), [fi]); }); jest.mocked(fileToGalleryItem).mockImplementation((file) => ({ height: 100, id: file.id || '', lastPictureUpdate: 0, mime_type: file.mime_type, name: file.name, type: 'image', uri: file.uri || '', width: file.width, cacheKey: file.id || '', })); const {getByTestId} = render( , ); fireEvent.press(getByTestId('1-updateFileForGallery')); fireEvent.press(getByTestId('1-onPress')); expect(openGalleryAtIndex).toHaveBeenCalledWith( 'test-post-id-fileAttachments-test-location', 0, expect.arrayContaining([expect.objectContaining({id: '1', uri: 'updated'}), expect.objectContaining({id: '2', uri: 'original'})]), ); fireEvent.press(getByTestId('2-onPress')); expect(openGalleryAtIndex).toHaveBeenCalledWith( 'test-post-id-fileAttachments-test-location', 1, expect.arrayContaining([expect.objectContaining({id: '1', uri: 'updated'}), expect.objectContaining({id: '2', uri: 'original'})]), ); }); it('should update files for gallery on new props', () => { const {rerender, getByTestId} = render( , ); const newFilesInfo = [ TestHelper.fakeFileInfo({id: '1', name: 'image1.png', user_id: 'user1'}), ]; rerender( , ); expect(getByTestId('1')).toBeVisible(); }); it('should set inViewPort to true on ITEM_IN_VIEWPORT event', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '1', name: 'image1.png', user_id: 'user1'}), TestHelper.fakeFileInfo({id: '2', name: 'image2.png', user_id: 'user2'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1'), nonImages: fi.filter((f) => f.id === '2'), }), [fi]); }); const {getByTestId} = render( , ); expect(getByTestId('1-inViewPort')).toHaveTextContent('false'); expect(getByTestId('2-inViewPort')).toHaveTextContent('false'); act(() => { DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, {'unrelated-event': true}); }); expect(getByTestId('1-inViewPort')).toHaveTextContent('false'); expect(getByTestId('2-inViewPort')).toHaveTextContent('false'); act(() => { DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, {'test-location-test-post-id': true}); }); expect(getByTestId('1-inViewPort')).toHaveTextContent('true'); expect(getByTestId('2-inViewPort')).toHaveTextContent('true'); }); it('should ignore ITEM_IN_VIEWPORT event if not for the current post or location', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '1', name: 'image1.png', user_id: 'user1'}), TestHelper.fakeFileInfo({id: '2', name: 'image2.png', user_id: 'user2'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1'), nonImages: fi.filter((f) => f.id === '2'), }), [fi]); }); const baseProps = getBaseProps(); baseProps.filesInfo = filesInfo; baseProps.location = 'location1'; baseProps.postId = 'post1'; const {getByTestId, rerender} = render(); expect(getByTestId('1-inViewPort')).toHaveTextContent('false'); expect(getByTestId('2-inViewPort')).toHaveTextContent('false'); baseProps.location = 'location2'; rerender(); expect(getByTestId('1-inViewPort')).toHaveTextContent('false'); expect(getByTestId('2-inViewPort')).toHaveTextContent('false'); act(() => { DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, {'location1-post1': true}); }); expect(getByTestId('1-inViewPort')).toHaveTextContent('false'); expect(getByTestId('2-inViewPort')).toHaveTextContent('false'); baseProps.postId = 'post2'; rerender(); expect(getByTestId('1-inViewPort')).toHaveTextContent('false'); expect(getByTestId('2-inViewPort')).toHaveTextContent('false'); act(() => { DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, {'location2-post1': true}); }); expect(getByTestId('1-inViewPort')).toHaveTextContent('false'); expect(getByTestId('2-inViewPort')).toHaveTextContent('false'); act(() => { DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, {'location2-post2': true}); }); expect(getByTestId('1-inViewPort')).toHaveTextContent('true'); expect(getByTestId('2-inViewPort')).toHaveTextContent('true'); }); it('should pass isSingleImage to the children', () => { let filesInfo = [ TestHelper.fakeFileInfo({id: '1'}), TestHelper.fakeFileInfo({id: '3'}), TestHelper.fakeFileInfo({id: '4'}), ]; const selectImages = (f: FileInfo | FileModel | undefined) => f?.id === '1' || f?.id === '2'; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => selectImages(f)), nonImages: fi.filter((f) => !selectImages(f)), }), [fi]); }); jest.mocked(isImage).mockImplementation((f) => selectImages(f)); jest.mocked(isVideo).mockReturnValue(false); const {rerender, getByTestId} = render( , ); expect(getByTestId('1-isSingleImage')).toHaveTextContent('true'); expect(getByTestId('3-isSingleImage')).toHaveTextContent('true'); expect(getByTestId('4-isSingleImage')).toHaveTextContent('true'); jest.mocked(isImage).mockReturnValue(false); jest.mocked(isVideo).mockImplementation((f) => selectImages(f)); rerender( , ); expect(getByTestId('1-isSingleImage')).toHaveTextContent('true'); expect(getByTestId('3-isSingleImage')).toHaveTextContent('true'); expect(getByTestId('4-isSingleImage')).toHaveTextContent('true'); filesInfo = [ TestHelper.fakeFileInfo({id: '1'}), TestHelper.fakeFileInfo({id: '2'}), TestHelper.fakeFileInfo({id: '3'}), TestHelper.fakeFileInfo({id: '4'}), ]; jest.mocked(isImage).mockImplementation((f) => selectImages(f)); jest.mocked(isVideo).mockReturnValue(false); rerender( , ); expect(getByTestId('1-isSingleImage')).toHaveTextContent('false'); expect(getByTestId('2-isSingleImage')).toHaveTextContent('false'); expect(getByTestId('3-isSingleImage')).toHaveTextContent('false'); expect(getByTestId('4-isSingleImage')).toHaveTextContent('false'); jest.mocked(isImage).mockReturnValue(false); jest.mocked(isVideo).mockImplementation((f) => selectImages(f)); rerender( , ); expect(getByTestId('1-isSingleImage')).toHaveTextContent('false'); expect(getByTestId('2-isSingleImage')).toHaveTextContent('false'); expect(getByTestId('3-isSingleImage')).toHaveTextContent('false'); expect(getByTestId('4-isSingleImage')).toHaveTextContent('false'); }); it('should trim more than 4 images and properly add the non visible images count to the last image', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '1'}), TestHelper.fakeFileInfo({id: '2'}), TestHelper.fakeFileInfo({id: '3'}), TestHelper.fakeFileInfo({id: '4'}), TestHelper.fakeFileInfo({id: '5'}), TestHelper.fakeFileInfo({id: '6'}), TestHelper.fakeFileInfo({id: '7'}), TestHelper.fakeFileInfo({id: '8'}), TestHelper.fakeFileInfo({id: '9'}), TestHelper.fakeFileInfo({id: '10'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1' || f.id === '2' || f.id === '3' || f.id === '4' || f.id === '5'), nonImages: fi.filter((f) => f.id === '6' || f.id === '7' || f.id === '8' || f.id === '9' || f.id === '10'), }), [fi]); }); const {getByTestId, queryByTestId} = render( , ); expect(getByTestId('1-nonVisibleImagesCount')).toHaveTextContent('undefined'); expect(getByTestId('2-nonVisibleImagesCount')).toHaveTextContent('undefined'); expect(getByTestId('3-nonVisibleImagesCount')).toHaveTextContent('undefined'); expect(getByTestId('4-nonVisibleImagesCount')).toHaveTextContent('1'); expect(queryByTestId('5-nonVisibleImagesCount')).not.toBeVisible(); expect(getByTestId('6-nonVisibleImagesCount')).toHaveTextContent('undefined'); expect(getByTestId('7-nonVisibleImagesCount')).toHaveTextContent('undefined'); expect(getByTestId('8-nonVisibleImagesCount')).toHaveTextContent('undefined'); expect(getByTestId('9-nonVisibleImagesCount')).toHaveTextContent('undefined'); expect(getByTestId('10-nonVisibleImagesCount')).toHaveTextContent('undefined'); }); it('should add gutter to the container of to all elements but the first only on image row', () => { const filesInfo = [ TestHelper.fakeFileInfo({id: '1'}), TestHelper.fakeFileInfo({id: '2'}), TestHelper.fakeFileInfo({id: '3'}), TestHelper.fakeFileInfo({id: '4'}), TestHelper.fakeFileInfo({id: '5'}), TestHelper.fakeFileInfo({id: '6'}), ]; jest.mocked(useImageAttachments).mockImplementation((fi) => { return useMemo(() => ({ images: fi.filter((f) => f.id === '1' || f.id === '2' || f.id === '3'), nonImages: fi.filter((f) => f.id === '4' || f.id === '5' || f.id === '6'), }), [fi]); }); const {getByTestId} = render( , ); expect(getByTestId('1-file-container')).not.toHaveStyle({marginLeft: 8}); expect(getByTestId('2-file-container')).toHaveStyle({marginLeft: 8}); expect(getByTestId('3-file-container')).toHaveStyle({marginLeft: 8}); expect(getByTestId('4-file-container')).not.toHaveStyle({marginLeft: 8}); expect(getByTestId('5-file-container')).not.toHaveStyle({marginLeft: 8}); expect(getByTestId('6-file-container')).not.toHaveStyle({marginLeft: 8}); }); });