import { renderHook, act } from '@testing-library/react-native' import { useDownloadMedia } from './use-download-media' import * as FileSystem from 'expo-file-system' import * as MediaLibrary from 'expo-media-library' jest.mock('expo-file-system', () => ({ documentDirectory: 'file:///mock/document/', createDownloadResumable: jest.fn(), deleteAsync: jest.fn(), })) jest.mock('expo-media-library', () => ({ requestPermissionsAsync: jest.fn(), saveToLibraryAsync: jest.fn(), })) describe('useDownloadMedia', () => { beforeEach(() => { jest.clearAllMocks() }) afterEach(() => { jest.restoreAllMocks() }) describe('initial state', () => { it('should return initial state', () => { const { result } = renderHook(() => useDownloadMedia()) expect(result.current.loading).toBe(false) expect(result.current.error).toBeNull() expect(result.current.progress).toBe(0) expect(typeof result.current.download).toBe('function') }) }) describe('permission handling', () => { it('should return error when permission is denied', async () => { ;(MediaLibrary.requestPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'denied', }) const { result } = renderHook(() => useDownloadMedia()) let response await act(async () => { response = await result.current.download('https://example.com/image.jpg', 'image') }) expect(response).toEqual({ success: false, error: 'Permission denied' }) expect(result.current.error).toBe('Permission denied') expect(result.current.loading).toBe(false) }) it('should proceed when permission is granted', async () => { ;(MediaLibrary.requestPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted', }) const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.jpg' }) ;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({ downloadAsync: mockDownloadAsync, }) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined) const { result } = renderHook(() => useDownloadMedia()) let response await act(async () => { response = await result.current.download('https://example.com/image.jpg', 'image') }) expect(response).toEqual({ success: true }) expect(MediaLibrary.requestPermissionsAsync).toHaveBeenCalled() }) }) describe('download functionality', () => { beforeEach(() => { ;(MediaLibrary.requestPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted', }) }) it('should download image successfully', async () => { const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.jpg' }) ;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({ downloadAsync: mockDownloadAsync, }) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined) const { result } = renderHook(() => useDownloadMedia()) let response await act(async () => { response = await result.current.download('https://example.com/image.jpg', 'image') }) expect(response).toEqual({ success: true }) expect(FileSystem.createDownloadResumable).toHaveBeenCalledWith( 'https://example.com/image.jpg', expect.stringMatching(/^file:\/\/\/mock\/document\/download_\d+\.jpg$/), {}, expect.any(Function) ) expect(MediaLibrary.saveToLibraryAsync).toHaveBeenCalled() expect(FileSystem.deleteAsync).toHaveBeenCalledWith( expect.stringMatching(/^file:\/\/\/mock\/document\/download_\d+\.jpg$/), { idempotent: true } ) }) it('should download video successfully', async () => { const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.mp4' }) ;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({ downloadAsync: mockDownloadAsync, }) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined) const { result } = renderHook(() => useDownloadMedia()) let response await act(async () => { response = await result.current.download('https://example.com/video.mp4', 'video') }) expect(response).toEqual({ success: true }) expect(FileSystem.createDownloadResumable).toHaveBeenCalledWith( 'https://example.com/video.mp4', expect.stringMatching(/^file:\/\/\/mock\/document\/download_\d+\.mp4$/), {}, expect.any(Function) ) }) it('should handle download failure', async () => { const mockDownloadAsync = jest.fn().mockRejectedValue(new Error('Network error')) ;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({ downloadAsync: mockDownloadAsync, }) const { result } = renderHook(() => useDownloadMedia()) let response await act(async () => { response = await result.current.download('https://example.com/image.jpg', 'image') }) expect(response).toEqual({ success: false, error: 'Network error' }) expect(result.current.error).toBe('Network error') }) it('should handle save to library failure', async () => { const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.jpg' }) ;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({ downloadAsync: mockDownloadAsync, }) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockRejectedValue(new Error('Save failed')) ;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined) const { result } = renderHook(() => useDownloadMedia()) let response await act(async () => { response = await result.current.download('https://example.com/image.jpg', 'image') }) expect(response).toEqual({ success: false, error: 'Save failed' }) expect(result.current.error).toBe('Save failed') // Should still attempt to clean up temp file expect(FileSystem.deleteAsync).toHaveBeenCalled() }) }) describe('loading state', () => { beforeEach(() => { ;(MediaLibrary.requestPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted', }) }) it('should set loading to true during download', async () => { let resolveDownload: (value: any) => void const downloadPromise = new Promise((resolve) => { resolveDownload = resolve }) const mockDownloadAsync = jest.fn().mockReturnValue(downloadPromise) ;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({ downloadAsync: mockDownloadAsync, }) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined) const { result } = renderHook(() => useDownloadMedia()) act(() => { result.current.download('https://example.com/image.jpg', 'image') }) expect(result.current.loading).toBe(true) await act(async () => { resolveDownload!({ uri: 'file:///mock/document/download_123.jpg' }) await downloadPromise }) expect(result.current.loading).toBe(false) }) it('should set loading to false after error', async () => { const mockDownloadAsync = jest.fn().mockRejectedValue(new Error('Error')) ;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({ downloadAsync: mockDownloadAsync, }) const { result } = renderHook(() => useDownloadMedia()) await act(async () => { await result.current.download('https://example.com/image.jpg', 'image') }) expect(result.current.loading).toBe(false) }) }) describe('progress tracking', () => { beforeEach(() => { ;(MediaLibrary.requestPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted', }) }) it('should update progress during download', async () => { let progressCallback: (progress: { totalBytesWritten: number; totalBytesExpectedToWrite: number }) => void const mockDownloadAsync = jest.fn().mockImplementation(async () => { // Simulate progress updates progressCallback({ totalBytesWritten: 50, totalBytesExpectedToWrite: 100 }) return { uri: 'file:///mock/document/download_123.jpg' } }) ;(FileSystem.createDownloadResumable as jest.Mock).mockImplementation( (url, fileUri, options, callback) => { progressCallback = callback return { downloadAsync: mockDownloadAsync } } ) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined) const { result } = renderHook(() => useDownloadMedia()) await act(async () => { await result.current.download('https://example.com/image.jpg', 'image') }) // Progress should be reset after completion expect(result.current.progress).toBe(0) }) it('should reset progress on new download', async () => { const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.jpg' }) ;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({ downloadAsync: mockDownloadAsync, }) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined) const { result } = renderHook(() => useDownloadMedia()) await act(async () => { await result.current.download('https://example.com/image1.jpg', 'image') }) expect(result.current.progress).toBe(0) await act(async () => { await result.current.download('https://example.com/image2.jpg', 'image') }) expect(result.current.progress).toBe(0) }) }) describe('error handling', () => { it('should clear error on new download attempt', async () => { ;(MediaLibrary.requestPermissionsAsync as jest.Mock) .mockResolvedValueOnce({ status: 'denied' }) .mockResolvedValueOnce({ status: 'granted' }) const mockDownloadAsync = jest.fn().mockResolvedValue({ uri: 'file:///mock/document/download_123.jpg' }) ;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({ downloadAsync: mockDownloadAsync, }) ;(MediaLibrary.saveToLibraryAsync as jest.Mock).mockResolvedValue(undefined) ;(FileSystem.deleteAsync as jest.Mock).mockResolvedValue(undefined) const { result } = renderHook(() => useDownloadMedia()) await act(async () => { await result.current.download('https://example.com/image.jpg', 'image') }) expect(result.current.error).toBe('Permission denied') await act(async () => { await result.current.download('https://example.com/image.jpg', 'image') }) expect(result.current.error).toBeNull() }) it('should handle unknown error types', async () => { ;(MediaLibrary.requestPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted', }) const mockDownloadAsync = jest.fn().mockRejectedValue('Unknown error') ;(FileSystem.createDownloadResumable as jest.Mock).mockReturnValue({ downloadAsync: mockDownloadAsync, }) const { result } = renderHook(() => useDownloadMedia()) let response await act(async () => { response = await result.current.download('https://example.com/image.jpg', 'image') }) expect(response).toEqual({ success: false, error: 'Download failed' }) expect(result.current.error).toBe('Download failed') }) }) })