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',
})
})
})
})
})
})