import type { ComponentProps } from 'react' import type { useLocalFileUploader } from './hooks' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { ALLOW_FILE_EXTENSIONS } from '@/types/app' import Uploader from './uploader' type LocalUploaderArgs = Parameters[0] const mocks = vi.hoisted(() => ({ hookArgs: undefined as LocalUploaderArgs | undefined, handleLocalFileUpload: vi.fn<(file: File) => void>(), })) vi.mock('./hooks', () => ({ useLocalFileUploader: (args: LocalUploaderArgs) => { mocks.hookArgs = args return { handleLocalFileUpload: mocks.handleLocalFileUpload, } }, })) const getInput = () => { const input = screen.getByTestId('local-file-input') return input as HTMLInputElement } const renderUploader = (props: Partial> = {}) => { const onUpload = vi.fn() const closePopover = vi.fn() const childRenderer = vi.fn((hovering: boolean) => (
{hovering ? 'hovering' : 'idle'}
)) const result = render( {childRenderer} , ) return { ...result, onUpload, closePopover, childRenderer, } } describe('Uploader', () => { beforeEach(() => { vi.clearAllMocks() mocks.hookArgs = undefined }) describe('Rendering', () => { it('should render file input and idle child content', () => { renderUploader() const input = getInput() expect(screen.getByTestId('hover-state')).toHaveTextContent('idle') expect(input).toBeInTheDocument() }) it('should set accept attribute from allowed file extensions', () => { renderUploader() const input = getInput() const expectedAccept = ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',') expect(input).toHaveAttribute('accept', expectedAccept) }) it('should pass hook arguments to useLocalFileUploader', () => { const { onUpload } = renderUploader({ limit: 5, disabled: true }) expect(mocks.hookArgs).toMatchObject({ limit: 5, disabled: true, }) expect(mocks.hookArgs?.onUpload).toBe(onUpload) }) }) describe('User Interactions', () => { it('should update hovering state on mouse enter and leave', async () => { const user = userEvent.setup() renderUploader() const input = getInput() expect(screen.getByTestId('hover-state')).toHaveTextContent('idle') await user.hover(input) expect(screen.getByTestId('hover-state')).toHaveTextContent('hovering') await user.unhover(input) expect(screen.getByTestId('hover-state')).toHaveTextContent('idle') }) it('should call handleLocalFileUpload and closePopover when file is selected', async () => { const user = userEvent.setup() const { closePopover } = renderUploader() const input = getInput() const file = new File(['hello'], 'demo.png', { type: 'image/png' }) await user.upload(input, file) expect(mocks.handleLocalFileUpload).toHaveBeenCalledWith(file) expect(closePopover).toHaveBeenCalledTimes(1) }) it('should reset input value on click', async () => { const user = userEvent.setup() renderUploader() const input = getInput() const file = new File(['hello'], 'demo.png', { type: 'image/png' }) await user.upload(input, file) expect(input.files).toHaveLength(1) await user.click(input) expect(input.value).toBe('') }) it('should not upload or close popover when no file is selected', () => { const { closePopover } = renderUploader() const input = getInput() Object.defineProperty(input, 'files', { value: [] as unknown as FileList, configurable: true, }) input.dispatchEvent(new Event('change', { bubbles: true })) expect(mocks.handleLocalFileUpload).not.toHaveBeenCalled() expect(closePopover).not.toHaveBeenCalled() }) }) describe('Props', () => { it('should disable file input when disabled prop is true', () => { renderUploader({ disabled: true }) const input = getInput() expect(input).toBeDisabled() }) }) })