import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { vi, describe, it, expect, beforeEach, beforeAll } from 'vitest' import { render } from '../../../tests/helpers/render' import { resetAllStores } from '../../../tests/helpers/store' import { PhotoUpload } from './PhotoUpload' beforeAll(() => { Object.defineProperty(URL, 'createObjectURL', { value: vi.fn(() => 'blob:mock'), writable: true }) Object.defineProperty(URL, 'revokeObjectURL', { value: vi.fn(), writable: true }) }) const defaultProps = { tripId: 1, days: [{ id: 1, day_number: 1, date: null }], places: [{ id: 1, name: 'Eiffel Tower' }], onUpload: vi.fn().mockResolvedValue(undefined), onClose: vi.fn(), } function makeFile(name = 'photo.jpg', type = 'image/jpeg') { return new File(['(binary)'], name, { type }) } async function uploadFiles(files: File[]) { const input = document.querySelector('input[type="file"]') as HTMLInputElement await userEvent.upload(input, files) } /** The upload/submit button is always the last button in the DOM. */ function getSubmitButton() { const buttons = screen.getAllByRole('button') return buttons[buttons.length - 1] } describe('PhotoUpload', () => { beforeEach(() => { resetAllStores() vi.clearAllMocks() defaultProps.onUpload = vi.fn().mockResolvedValue(undefined) defaultProps.onClose = vi.fn() }) it('FE-COMP-PHOTOUPLOAD-001: renders dropzone with upload instructions', () => { render() expect(screen.getByText('Drop photos here')).toBeInTheDocument() // Upload icon rendered via lucide-react as SVG expect(document.querySelector('svg')).toBeTruthy() }) it('FE-COMP-PHOTOUPLOAD-002: options section hidden before files are selected', () => { render() expect(screen.queryByText('Link Day')).not.toBeInTheDocument() expect(screen.queryByPlaceholderText('Optional caption...')).not.toBeInTheDocument() }) it('FE-COMP-PHOTOUPLOAD-003: upload button is disabled when no files selected', () => { render() // The upload button is the last button and should be disabled with no files const uploadBtn = getSubmitButton() expect(uploadBtn).toBeDisabled() }) it('FE-COMP-PHOTOUPLOAD-004: selecting a file shows preview and reveals options', async () => { render() await uploadFiles([makeFile()]) expect(screen.getByAltText('photo.jpg')).toBeInTheDocument() expect(screen.getByText('Link Day')).toBeInTheDocument() expect(screen.getByPlaceholderText('Optional caption...')).toBeInTheDocument() }) it('FE-COMP-PHOTOUPLOAD-005: file count label updates correctly', async () => { render() await uploadFiles([makeFile('photo1.jpg'), makeFile('photo2.jpg')]) expect(screen.getByText('2 Photos selected')).toBeInTheDocument() }) it('FE-COMP-PHOTOUPLOAD-006: remove button removes a file from preview', async () => { render() await uploadFiles([makeFile('photo1.jpg'), makeFile('photo2.jpg')]) expect(screen.getByText('2 Photos selected')).toBeInTheDocument() // Remove buttons are inside `.relative.aspect-square` wrappers in the preview grid const removeButtons = document.querySelectorAll('.relative.aspect-square button') expect(removeButtons.length).toBe(2) await userEvent.click(removeButtons[0]) expect(screen.getByText('1 Photo selected')).toBeInTheDocument() expect(screen.getAllByRole('img').length).toBe(1) }) it('FE-COMP-PHOTOUPLOAD-007: upload button calls onUpload with FormData', async () => { render() const file = makeFile() await uploadFiles([file]) await userEvent.click(getSubmitButton()) expect(defaultProps.onUpload).toHaveBeenCalledOnce() const formData = defaultProps.onUpload.mock.calls[0][0] as FormData expect(formData).toBeInstanceOf(FormData) expect(formData.get('photos')).toBe(file) }) it('FE-COMP-PHOTOUPLOAD-008: day selection adds day_id to FormData', async () => { render() await uploadFiles([makeFile()]) // First combobox is the day selector; select day id=1 const selects = screen.getAllByRole('combobox') await userEvent.selectOptions(selects[0], '1') await userEvent.click(getSubmitButton()) const formData = defaultProps.onUpload.mock.calls[0][0] as FormData expect(formData.get('day_id')).toBe('1') }) it('FE-COMP-PHOTOUPLOAD-009: caption field adds caption to FormData', async () => { render() await uploadFiles([makeFile()]) await userEvent.type(screen.getByPlaceholderText('Optional caption...'), 'Vacation') await userEvent.click(getSubmitButton()) const formData = defaultProps.onUpload.mock.calls[0][0] as FormData expect(formData.get('caption')).toBe('Vacation') }) it('FE-COMP-PHOTOUPLOAD-010: cancel button calls onClose', async () => { render() const cancelBtn = screen.getByRole('button', { name: /abbrechen|cancel/i }) await userEvent.click(cancelBtn) expect(defaultProps.onClose).toHaveBeenCalledOnce() }) it('FE-COMP-PHOTOUPLOAD-011: upload in progress shows spinner and disables button', async () => { let resolveUpload!: () => void const pendingPromise = new Promise(resolve => { resolveUpload = resolve }) defaultProps.onUpload = vi.fn().mockReturnValue(pendingPromise) render() await uploadFiles([makeFile()]) await userEvent.click(getSubmitButton()) await waitFor(() => { expect(screen.getAllByText(/uploading/i).length).toBeGreaterThan(0) }) expect(getSubmitButton()).toBeDisabled() // Cleanup resolveUpload() }) })