import { render, screen, fireEvent, renderHook, act, } from '@testing-library/react' import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest' import {ImagePickerProvider, useImagePickerContext} from './ImagePickerProvider' // Mock useRequestPermissions hook const mockRequestPermission = vi.fn() vi.mock('../hooks/util/useRequestPermissions', () => ({ useRequestPermissions: () => ({ requestPermission: mockRequestPermission, }), })) // Mock useReportInteraction hook const mockReportInteraction = vi.fn() vi.mock('../internal/useReportInteraction', () => ({ useReportInteraction: () => ({ reportInteraction: mockReportInteraction, }), })) describe('ImagePickerProvider', () => { const originalMinisParams = window.minisParams beforeEach(() => { vi.clearAllMocks() // Default to granting permission mockRequestPermission.mockResolvedValue({granted: true}) // Clear interaction reporting mock mockReportInteraction.mockClear() // Mock URL.createObjectURL and URL.revokeObjectURL for jsdom global.URL.createObjectURL = vi.fn(() => 'blob:mock-url') global.URL.revokeObjectURL = vi.fn() // Mock Image constructor for resizing global.Image = class MockImage { width = 1920 height = 1080 onload = null as any onerror = null as any src = '' constructor() { setTimeout(() => { if (this.onload) { this.onload() } }, 0) } } as any // Mock canvas methods HTMLCanvasElement.prototype.getContext = vi.fn(() => ({ drawImage: vi.fn(), })) as any HTMLCanvasElement.prototype.toBlob = vi.fn(callback => { const blob = new Blob(['mock'], {type: 'image/jpeg'}) callback(blob) }) as any }) afterEach(() => { // Restore original minisParams window.minisParams = originalMinisParams }) describe('Context', () => { it('throws error when used outside of provider', () => { // Silence console.error for this test const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) expect(() => { renderHook(() => useImagePickerContext()) }).toThrow( 'useImagePickerContext must be used within an ImagePickerProvider' ) consoleSpy.mockRestore() }) it('provides context value when used within provider', () => { const {result} = renderHook(() => useImagePickerContext(), { wrapper: ({children}: {children: React.ReactNode}) => ( {children} ), }) expect(result.current).toHaveProperty('openCamera') expect(result.current).toHaveProperty('openGallery') expect(typeof result.current.openCamera).toBe('function') expect(typeof result.current.openGallery).toBe('function') }) }) describe('Hidden Inputs', () => { it('renders hidden file inputs', () => { const {container} = render(
Test Content
) const inputs = container.querySelectorAll('input[type="file"]') expect(inputs).toHaveLength(3) // Gallery input const galleryInput = inputs[0] expect(galleryInput).toHaveAttribute('accept', 'image/*') expect(galleryInput).not.toHaveAttribute('capture') expect(galleryInput).toHaveStyle({display: 'none'}) // Front camera input const frontCameraInput = inputs[1] expect(frontCameraInput).toHaveAttribute('accept', 'image/*') expect(frontCameraInput).toHaveAttribute('capture', 'user') // Back camera input const backCameraInput = inputs[2] expect(backCameraInput).toHaveAttribute('accept', 'image/*') expect(backCameraInput).toHaveAttribute('capture', 'environment') }) }) describe('openGallery', () => { it('requests CAMERA permission before showing picker', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openGallery} = useImagePickerContext() return ( ) } const {container} = render( ) const galleryInput = container.querySelector( 'input[type="file"]:not([capture])' ) as HTMLInputElement const clickSpy = vi.spyOn(galleryInput, 'click') // Click the button to open gallery const button = screen.getByText('Open Gallery') fireEvent.click(button) // Wait for permission request to complete await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalledWith({ permission: 'CAMERA', }) }) // Input should be clicked after permission request await vi.waitFor(() => { expect(clickSpy).toHaveBeenCalledTimes(1) }) // Simulate file selection const file = new File(['test'], 'test.jpg', {type: 'image/jpeg'}) Object.defineProperty(galleryInput, 'files', { value: [file], configurable: true, }) fireEvent.change(galleryInput) // Check that the promise resolved with the file await vi.waitFor(() => { const selectedFile = document.querySelector( '[data-testid="selected-file"]' ) expect(selectedFile?.textContent).toBe('test.jpg') }) }) it('handles cancel event', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openGallery} = useImagePickerContext() return ( ) } const {container} = render( ) const galleryInput = container.querySelector( 'input[type="file"]:not([capture])' ) as HTMLInputElement const button = screen.getByText('Open Gallery') fireEvent.click(button) // Wait for permission request to complete await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalledWith({ permission: 'CAMERA', }) }) // Simulate cancel event await act(async () => { const cancelEvent = new Event('cancel', {bubbles: true}) galleryInput.dispatchEvent(cancelEvent) }) await vi.waitFor(() => { const cancelMessage = document.querySelector( '[data-testid="cancel-message"]' ) expect(cancelMessage?.textContent).toBe('User cancelled file selection') }) }) it('still opens picker even if CAMERA permission is denied', async () => { mockRequestPermission.mockRejectedValue(new Error('Permission denied')) const TestComponent = () => { const {openGallery} = useImagePickerContext() return ( ) } const {container} = render( ) const galleryInput = container.querySelector( 'input[type="file"]:not([capture])' ) as HTMLInputElement const clickSpy = vi.spyOn(galleryInput, 'click') const button = screen.getByText('Open Gallery') fireEvent.click(button) // Wait for permission request to complete await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalledWith({ permission: 'CAMERA', }) }) // Input should still be clicked even after permission rejection await vi.waitFor(() => { expect(clickSpy).toHaveBeenCalledTimes(1) }) }) }) describe('openCamera', () => { it('requests CAMERA permission and opens camera if granted', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openCamera} = useImagePickerContext() return ( ) } const {container} = render( ) const backCameraInput = container.querySelector( 'input[capture="environment"]' ) as HTMLInputElement const clickSpy = vi.spyOn(backCameraInput, 'click') const button = screen.getByText('Open Camera') fireEvent.click(button) // Wait for permission request to complete await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalledWith({ permission: 'CAMERA', }) }) // Input should be clicked after permission is granted await vi.waitFor(() => { expect(clickSpy).toHaveBeenCalledTimes(1) }) }) it('triggers front camera input click when specified', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openCamera} = useImagePickerContext() return ( ) } const {container} = render( ) const frontCameraInput = container.querySelector( 'input[capture="user"]' ) as HTMLInputElement const clickSpy = vi.spyOn(frontCameraInput, 'click') const button = screen.getByText('Open Front Camera') fireEvent.click(button) // Wait for permission request to complete await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalledWith({ permission: 'CAMERA', }) }) // Front camera input should be clicked after permission is granted await vi.waitFor(() => { expect(clickSpy).toHaveBeenCalledTimes(1) }) }) it('resolves with selected file from camera', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openCamera} = useImagePickerContext() return ( ) } const {container} = render( ) const cameraInput = container.querySelector( 'input[capture="environment"]' ) as HTMLInputElement const button = screen.getByText('Open Camera') fireEvent.click(button) // Wait for permission request to complete await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalledWith({ permission: 'CAMERA', }) }) // Simulate file capture const file = new File(['photo'], 'photo.jpg', {type: 'image/jpeg'}) Object.defineProperty(cameraInput, 'files', { value: [file], configurable: true, }) fireEvent.change(cameraInput) await vi.waitFor(() => { const cameraFile = document.querySelector('[data-testid="camera-file"]') expect(cameraFile?.textContent).toBe('photo.jpg') }) }) it('handles cancel event for camera', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openCamera} = useImagePickerContext() return ( ) } const {container} = render( ) const cameraInput = container.querySelector( 'input[capture="environment"]' ) as HTMLInputElement const button = screen.getByText('Open Camera') fireEvent.click(button) // Wait for permission request to complete await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalledWith({ permission: 'CAMERA', }) }) // Simulate cancel event await act(async () => { const cancelEvent = new Event('cancel', {bubbles: true}) cameraInput.dispatchEvent(cancelEvent) }) await vi.waitFor(() => { const cancelMessage = document.querySelector( '[data-testid="camera-cancel"]' ) expect(cancelMessage?.textContent).toBe('User cancelled camera') }) }) it('rejects with error if CAMERA permission is not granted', async () => { mockRequestPermission.mockResolvedValue({granted: false}) const TestComponent = () => { const {openCamera} = useImagePickerContext() return ( ) } const {container} = render( ) const cameraInput = container.querySelector( 'input[capture="environment"]' ) as HTMLInputElement const clickSpy = vi.spyOn(cameraInput, 'click') const button = screen.getByText('Open Camera') fireEvent.click(button) // Wait for permission request to complete await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalledWith({ permission: 'CAMERA', }) }) // Input should NOT be clicked when permission is denied expect(clickSpy).not.toHaveBeenCalled() // Should show error message await vi.waitFor(() => { const errorMessage = document.querySelector( '[data-testid="permission-error"]' ) expect(errorMessage?.textContent).toBe('Camera permission not granted') }) }) it('rejects with error if permission request fails', async () => { mockRequestPermission.mockRejectedValue( new Error('Permission request failed') ) const TestComponent = () => { const {openCamera} = useImagePickerContext() return ( ) } const {container} = render( ) const cameraInput = container.querySelector( 'input[capture="environment"]' ) as HTMLInputElement const clickSpy = vi.spyOn(cameraInput, 'click') const button = screen.getByText('Open Camera') fireEvent.click(button) // Wait for permission request to complete await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalledWith({ permission: 'CAMERA', }) }) // Input should NOT be clicked when permission request fails expect(clickSpy).not.toHaveBeenCalled() // Should show error message await vi.waitFor(() => { const errorMessage = document.querySelector( '[data-testid="permission-error"]' ) expect(errorMessage?.textContent).toBe('Camera permission not granted') }) }) }) describe('Multiple Picker Handling', () => { it('rejects previous promise when new picker is opened', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openCamera, openGallery} = useImagePickerContext() return ( <> ) } render( ) // Open gallery first const galleryButton = screen.getByText('Open Gallery') fireEvent.click(galleryButton) // Then immediately open camera const cameraButton = screen.getByText('Open Camera') fireEvent.click(cameraButton) await vi.waitFor(() => { const errorMessage = document.querySelector( '[data-testid="gallery-error"]' ) expect(errorMessage?.textContent).toBe( 'New file picker opened before previous completed' ) }) }) }) describe('Cleanup', () => { it('clears input value after file selection', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openGallery} = useImagePickerContext() return ( ) } const {container} = render( ) const galleryInput = container.querySelector( 'input[type="file"]:not([capture])' ) as HTMLInputElement const button = screen.getByText('Open Gallery') fireEvent.click(button) // Wait for permission request to complete await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalledWith({ permission: 'CAMERA', }) }) // Set file and trigger change const file = new File(['test'], 'test.jpg', {type: 'image/jpeg'}) Object.defineProperty(galleryInput, 'files', { value: [file], configurable: true, }) fireEvent.change(galleryInput) // Check that input value was cleared expect(galleryInput.value).toBe('') }) }) describe('Interaction Reporting', () => { describe('Gallery interactions', () => { it('reports image_picker_open when gallery is opened', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openGallery} = useImagePickerContext() return ( ) } render( ) const button = screen.getByText('Open Gallery') fireEvent.click(button) // Wait for interaction to be reported await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'image_picker_open', }) }) }) it('reports image_picker_success when file is selected', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openGallery} = useImagePickerContext() return ( ) } const {container} = render( ) const galleryInput = container.querySelector( 'input[type="file"]:not([capture])' ) as HTMLInputElement const button = screen.getByText('Open Gallery') fireEvent.click(button) // Wait for permission and initial interaction await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'image_picker_open', }) }) // Clear mock to check only for success interaction mockReportInteraction.mockClear() // Simulate file selection const file = new File(['test'], 'test.jpg', {type: 'image/jpeg'}) Object.defineProperty(galleryInput, 'files', { value: [file], configurable: true, }) await act(async () => { fireEvent.change(galleryInput) }) // Check success was reported await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'image_picker_success', }) }) }) it('reports image_picker_error when interrupted by new picker', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openGallery, openCamera} = useImagePickerContext() return ( <> ) } render( ) // Open gallery first const galleryButton = screen.getByText('Open Gallery') fireEvent.click(galleryButton) // Wait for gallery open to be reported await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'image_picker_open', }) }) mockReportInteraction.mockClear() // Open camera to interrupt gallery const cameraButton = screen.getByText('Open Camera') fireEvent.click(cameraButton) // Check error was reported for interrupted gallery operation await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'image_picker_error', interactionValue: 'New file picker opened before previous completed', }) }) }) }) describe('Camera interactions', () => { it('reports camera_open when camera is opened', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openCamera} = useImagePickerContext() return ( ) } render( ) const button = screen.getByText('Open Camera') fireEvent.click(button) // Wait for interaction to be reported await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'camera_open', }) }) }) it('reports camera_success when photo is captured', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openCamera} = useImagePickerContext() return ( ) } const {container} = render( ) const cameraInput = container.querySelector( 'input[capture="environment"]' ) as HTMLInputElement const button = screen.getByText('Open Camera') fireEvent.click(button) // Wait for camera open to be reported await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'camera_open', }) }) mockReportInteraction.mockClear() // Simulate photo capture const file = new File(['photo'], 'photo.jpg', {type: 'image/jpeg'}) Object.defineProperty(cameraInput, 'files', { value: [file], configurable: true, }) await act(async () => { fireEvent.change(cameraInput) }) // Check success was reported await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'camera_success', }) }) }) it('reports camera_error when interrupted by new picker', async () => { mockRequestPermission.mockResolvedValue({granted: true}) const TestComponent = () => { const {openCamera, openGallery} = useImagePickerContext() return ( <> ) } render( ) // Open camera first const cameraButton = screen.getByText('Open Camera') fireEvent.click(cameraButton) // Wait for camera open to be reported await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'camera_open', }) }) mockReportInteraction.mockClear() // Open gallery to interrupt camera const galleryButton = screen.getByText('Open Gallery') fireEvent.click(galleryButton) // Check error was reported for interrupted camera operation await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'camera_error', interactionValue: 'New file picker opened before previous completed', }) }) }) it('reports camera_error when permission is denied', async () => { mockRequestPermission.mockResolvedValue({granted: false}) const TestComponent = () => { const {openCamera} = useImagePickerContext() return ( ) } render( ) const button = screen.getByText('Open Camera') fireEvent.click(button) // Wait for permission request await vi.waitFor(() => { expect(mockRequestPermission).toHaveBeenCalled() }) // Camera open should be reported expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'camera_open', }) // Error interaction should be reported for permission denial await vi.waitFor(() => { expect(mockReportInteraction).toHaveBeenCalledWith({ interactionType: 'camera_error', interactionValue: 'Camera permission not granted', }) }) }) }) }) })