pegasus/frontend/src/UploaderView.test.tsx

179 lines
6.5 KiB
TypeScript
Raw Normal View History

2026-04-11 00:02:59 -03:00
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<string, unknown> = {}) {
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(<UploaderView />)
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(<UploaderView />)
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(<UploaderView />)
expect(screen.getByText(/No subfolders yet/)).toBeTruthy()
expect(screen.getByText('Empty.')).toBeTruthy()
})
})