mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
0fe1c443e9
Replace 6 hardcoded German strings in PhotoUpload.tsx with t() calls:
- 'Tag verknüpfen' → t('photos.linkDay')
- 'Kein Tag' / 'Tag N' → t('photos.noDay') / t('photos.dayLabel')
- '{N} Foto(s) ausgewählt' → t('photos.photoSelected/photosSelected')
- 'bis zu 30 Fotos' hint → t('photos.fileTypeHint')
- 'Wird hochgeladen...' → t('common.uploading')
Add all 6 new keys to all 14 language files and update test
assertions from German strings to English equivalents.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
5.8 KiB
TypeScript
158 lines
5.8 KiB
TypeScript
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(<PhotoUpload {...defaultProps} />)
|
|
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(<PhotoUpload {...defaultProps} />)
|
|
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(<PhotoUpload {...defaultProps} />)
|
|
// 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(<PhotoUpload {...defaultProps} />)
|
|
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(<PhotoUpload {...defaultProps} />)
|
|
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(<PhotoUpload {...defaultProps} />)
|
|
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(<PhotoUpload {...defaultProps} />)
|
|
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(<PhotoUpload {...defaultProps} />)
|
|
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(<PhotoUpload {...defaultProps} />)
|
|
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(<PhotoUpload {...defaultProps} />)
|
|
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<void>(resolve => { resolveUpload = resolve })
|
|
defaultProps.onUpload = vi.fn().mockReturnValue(pendingPromise)
|
|
|
|
render(<PhotoUpload {...defaultProps} />)
|
|
await uploadFiles([makeFile()])
|
|
|
|
await userEvent.click(getSubmitButton())
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getAllByText(/uploading/i).length).toBeGreaterThan(0)
|
|
})
|
|
|
|
expect(getSubmitButton()).toBeDisabled()
|
|
|
|
// Cleanup
|
|
resolveUpload()
|
|
})
|
|
})
|