Files
TREK/client/src/components/Photos/PhotoUpload.test.tsx
T
Isaias Tavares 0fe1c443e9 fix(i18n): translate remaining German hardcoded strings in PhotoUpload
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>
2026-04-12 16:53:50 -03:00

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()
})
})