212 lines
7.8 KiB
TypeScript
212 lines
7.8 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { afterEach, beforeAll, describe, expect, it, jest } from '@jest/globals'
|
|
|
|
const mockUseUploaderController = jest.fn()
|
|
|
|
jest.mock('./uploader-controller', () => ({
|
|
__esModule: true,
|
|
default: (...args: unknown[]) => mockUseUploaderController(...args),
|
|
}))
|
|
|
|
// Defer module evaluation until after mocks are registered.
|
|
const UploaderView = require('./UploaderView').default
|
|
|
|
function makeFile(name: string, type: string) {
|
|
return new File(['x'], name, { type })
|
|
}
|
|
|
|
function makeController(overrides: Record<string, unknown> = {}) {
|
|
const setSel = jest.fn()
|
|
const setBulkDesc = jest.fn()
|
|
const setGlobalDate = jest.fn()
|
|
const setLib = jest.fn()
|
|
const setSub = jest.fn()
|
|
const setNewFolderRaw = jest.fn()
|
|
const refresh = jest.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: jest.fn(),
|
|
applyDescToAllVideos: jest.fn(),
|
|
doUpload: jest.fn(),
|
|
createSubfolder: jest.fn(),
|
|
renameFolder: jest.fn(),
|
|
deleteFolder: jest.fn(),
|
|
renamePath: jest.fn(),
|
|
deletePath: jest.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: jest.fn(() => 'blob:thumb'), configurable: true })
|
|
} else {
|
|
jest.spyOn(URL, 'createObjectURL').mockReturnValue('blob:thumb')
|
|
}
|
|
if (!('revokeObjectURL' in URL)) {
|
|
Object.defineProperty(URL, 'revokeObjectURL', { value: jest.fn(), configurable: true })
|
|
} else {
|
|
jest.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {})
|
|
}
|
|
})
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
describe('UploaderView', () => {
|
|
it('renders populated state and forwards interactions', async () => {
|
|
const controller = makeController()
|
|
mockUseUploaderController.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' } })
|
|
fireEvent.change(screen.getByLabelText('Select file(s)'), {
|
|
target: { files: [makeFile('desktop.jpg', 'image/jpeg')] },
|
|
})
|
|
fireEvent.change(screen.getByLabelText('Select folder(s)'), {
|
|
target: { files: [makeFile('folder.mp4', 'video/mp4')] },
|
|
})
|
|
|
|
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.handleChoose).toHaveBeenCalled()
|
|
expect(controller.setSel).toHaveBeenCalled()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Apply to all videos' }))
|
|
fireEvent.change(screen.getByPlaceholderText('letters, numbers, underscores, dashes'), {
|
|
target: { value: 'renamed-folder' },
|
|
})
|
|
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.setNewFolderRaw).toHaveBeenCalledWith('renamed-folder')
|
|
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()
|
|
}, 15000)
|
|
|
|
it('renders mobile file pickers and forwards capture/gallery selection', () => {
|
|
const controller = makeController({
|
|
mobile: true,
|
|
sel: [],
|
|
sortedRows: [],
|
|
rootDirs: [],
|
|
videosNeedingDesc: 0,
|
|
})
|
|
mockUseUploaderController.mockReturnValue(controller)
|
|
|
|
render(<UploaderView />)
|
|
|
|
fireEvent.change(screen.getByLabelText('Gallery/Photos'), {
|
|
target: { files: [makeFile('photo.jpg', 'image/jpeg')] },
|
|
})
|
|
fireEvent.change(screen.getByLabelText('Camera (optional)'), {
|
|
target: { files: [makeFile('clip.mp4', 'video/mp4')] },
|
|
})
|
|
|
|
expect(controller.handleChoose).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('renders the empty-library state', () => {
|
|
mockUseUploaderController.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', () => {
|
|
mockUseUploaderController.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()
|
|
})
|
|
})
|