import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import UploaderView from './UploaderView' const controllerMock = vi.hoisted(() => ({ useUploaderController: vi.fn(), })) vi.mock('./uploader-controller', () => ({ default: controllerMock.useUploaderController, })) function makeFile(name: string, type: string) { return new File(['x'], name, { type }) } function makeController(overrides: Record = {}) { const setSel = vi.fn() const setBulkDesc = vi.fn() const setGlobalDate = vi.fn() const setLib = vi.fn() const setSub = vi.fn() const setNewFolderRaw = vi.fn() const refresh = vi.fn() return { mobile: false, me: { username: 'brad' }, libs: ['alpha', 'beta'], lib: 'alpha', sub: 'videos', rootDirs: ['archive', 'videos'], rows: [], status: 'Ready', globalDate: '2026-04-10', uploading: false, sel: [ { file: makeFile('photo.jpg', 'image/jpeg'), desc: '', date: '2026-04-10', finalName: '2026.04.10.upload.photo.jpg', progress: 0 }, { file: makeFile('clip.mp4', 'video/mp4'), desc: '', date: '2026-04-10', finalName: '2026.04.10.upload.clip.mp4', progress: 0 }, { file: makeFile('note.pdf', 'application/pdf'), desc: '', date: '2026-04-10', finalName: '2026.04.10.upload.note.pdf', progress: 0 }, ], bulkDesc: 'holiday', folderInputRef: { current: document.createElement('input') }, newFolderRaw: 'new-folder', setGlobalDate, setLib, setSub, setNewFolderRaw, setBulkDesc, setSel, handleChoose: vi.fn(), applyDescToAllVideos: vi.fn(), doUpload: vi.fn(), createSubfolder: vi.fn(), renameFolder: vi.fn(), deleteFolder: vi.fn(), renamePath: vi.fn(), deletePath: vi.fn(), refresh, sortedRows: [ { name: 'archive', path: 'archive', is_dir: true, size: 0, mtime: 0 }, { name: 'clip.mp4', path: 'clip.mp4', is_dir: false, size: 2048, mtime: 1713000000 }, ], existingNames: new Set(['clip.mp4']), duplicateNamesInSelection: new Set(['2026.04.10.upload.clip.mp4']), hasNameIssues: false, destPath: '/alpha/videos', videosNeedingDesc: 0, ...overrides, } } beforeAll(() => { if (!('createObjectURL' in URL)) { Object.defineProperty(URL, 'createObjectURL', { value: vi.fn(() => 'blob:thumb'), configurable: true }) } else { vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:thumb') } if (!('revokeObjectURL' in URL)) { Object.defineProperty(URL, 'revokeObjectURL', { value: vi.fn(), configurable: true }) } else { vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}) } }) afterEach(() => { vi.clearAllMocks() }) describe('UploaderView', () => { it('renders populated state and forwards interactions', async () => { const controller = makeController() controllerMock.useUploaderController.mockReturnValue(controller) render() await waitFor(() => expect(screen.getByText('Signed in: brad')).toBeTruthy()) expect(screen.getByText(/Destination:/)).toBeTruthy() expect(screen.getByText('Ready')).toBeTruthy() await waitFor(() => expect(document.querySelector('img')).not.toBeNull()) expect(document.querySelector('video')).not.toBeNull() expect(screen.getByText('📄')).toBeTruthy() fireEvent.change(screen.getByLabelText('Default date'), { target: { value: '2026-04-11' } }) fireEvent.change(screen.getByPlaceholderText('Short video description'), { target: { value: 'family trip' } }) const optionalImageInputs = screen.getAllByPlaceholderText('Optional for image') fireEvent.change(optionalImageInputs[0], { target: { value: 'photo desc' } }) fireEvent.change(optionalImageInputs[1], { target: { value: 'note desc' } }) fireEvent.change(screen.getByPlaceholderText('Required for video'), { target: { value: 'clip desc' } }) fireEvent.change(screen.getAllByDisplayValue('2026-04-10')[1], { target: { value: '2026-04-12' } }) fireEvent.change(screen.getByRole('combobox'), { target: { value: 'beta' } }) expect(controller.setGlobalDate).toHaveBeenCalledWith('2026-04-11') expect(controller.setBulkDesc).toHaveBeenCalledWith('family trip') expect(controller.setSel).toHaveBeenCalled() fireEvent.click(screen.getByRole('button', { name: 'Apply to all videos' })) fireEvent.click(screen.getByRole('button', { name: 'Create' })) fireEvent.click(screen.getByRole('button', { name: 'Rename' })) fireEvent.click(screen.getByLabelText('Go to library root')) fireEvent.click(screen.getByLabelText('Delete subfolder')) fireEvent.click(screen.getAllByRole('button', { name: 'Select' })[0]) fireEvent.click(screen.getByRole('button', { name: 'Open' })) fireEvent.click(screen.getByLabelText('Delete clip.mp4')) fireEvent.click(screen.getByRole('button', { name: /Upload \(3\)/ })) expect(controller.applyDescToAllVideos).toHaveBeenCalled() expect(controller.createSubfolder).toHaveBeenCalledWith('new-folder') expect(controller.renameFolder).toHaveBeenCalledWith('videos') expect(controller.refresh).toHaveBeenCalled() expect(controller.deleteFolder).toHaveBeenCalledWith('videos') expect(controller.deletePath).toHaveBeenCalledWith('clip.mp4', false) expect(controller.doUpload).toHaveBeenCalled() expect(screen.getByText('photo.jpg')).toBeTruthy() expect(screen.getByText('clip.mp4')).toBeTruthy() expect(screen.getByText('note.pdf')).toBeTruthy() }) it('renders the empty-library state', () => { controllerMock.useUploaderController.mockReturnValue( makeController({ lib: '', sub: '', rootDirs: [], sel: [], sortedRows: [], videosNeedingDesc: 0, hasNameIssues: false, destPath: '/(choose a library)', }) ) render() expect(screen.getByText('Select at least one file.')).toBeTruthy() expect(screen.getByText('Select a library to create subfolders and view contents.')).toBeTruthy() }) it('renders empty destination sections when the library has no children', () => { controllerMock.useUploaderController.mockReturnValue( makeController({ lib: 'alpha', sub: '', rootDirs: [], sel: [], sortedRows: [], destPath: '/alpha', videosNeedingDesc: 0, }) ) render() expect(screen.getByText(/No subfolders yet/)).toBeTruthy() expect(screen.getByText('Empty.')).toBeTruthy() }) })