mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 22:01:45 +00:00
test: expand frontend test suite to 82% coverage
Adds ~45 new and updated test files covering Admin, Collab, Dashboard, Map, Memories, PDF, Photos, Planner, Settings, Vacay, Weather components, pages, stores, and a WebSocket integration test.
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
||||
import { render } from '../../../tests/helpers/render'
|
||||
import { resetAllStores } from '../../../tests/helpers/store'
|
||||
import PhotoGallery from './PhotoGallery'
|
||||
|
||||
vi.mock('./PhotoLightbox', () => ({
|
||||
PhotoLightbox: ({ onClose, onDelete, photos, initialIndex }: any) => (
|
||||
<div data-testid="lightbox" data-index={initialIndex}>
|
||||
<button onClick={onClose}>close-lightbox</button>
|
||||
<button onClick={() => onDelete(photos[initialIndex]?.id)}>delete-photo</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./PhotoUpload', () => ({
|
||||
PhotoUpload: ({ onClose }: any) => (
|
||||
<div data-testid="photo-upload">
|
||||
<button onClick={onClose}>close-upload</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../shared/Modal', () => ({
|
||||
default: ({ isOpen, children }: any) =>
|
||||
isOpen ? <div data-testid="modal">{children}</div> : null,
|
||||
}))
|
||||
|
||||
const buildPhoto = (overrides = {}) => ({
|
||||
id: 1,
|
||||
url: '/uploads/photo1.jpg',
|
||||
caption: null,
|
||||
original_name: 'photo1.jpg',
|
||||
day_id: null,
|
||||
place_id: null,
|
||||
file_size: 102400,
|
||||
created_at: '2025-01-15T12:00:00Z',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
onUpload: vi.fn().mockResolvedValue(undefined),
|
||||
onDelete: vi.fn().mockResolvedValue(undefined),
|
||||
onUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
places: [],
|
||||
days: [],
|
||||
tripId: 1,
|
||||
}
|
||||
|
||||
describe('PhotoGallery', () => {
|
||||
beforeEach(() => {
|
||||
resetAllStores()
|
||||
vi.clearAllMocks()
|
||||
defaultProps.onUpload = vi.fn().mockResolvedValue(undefined)
|
||||
defaultProps.onDelete = vi.fn().mockResolvedValue(undefined)
|
||||
defaultProps.onUpdate = vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-001: shows photo count in header', () => {
|
||||
const photos = [buildPhoto(), buildPhoto({ id: 2 })]
|
||||
render(<PhotoGallery {...defaultProps} photos={photos} />)
|
||||
// The count paragraph renders "2 Fotos" as split text nodes
|
||||
expect(screen.getByText((content, el) => el?.tagName === 'P' && el.textContent?.trim().startsWith('2'))).toBeInTheDocument()
|
||||
expect(screen.getAllByText('Fotos').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-002: shows empty state when no photos', () => {
|
||||
render(<PhotoGallery {...defaultProps} photos={[]} />)
|
||||
// noPhotos key renders some text — check the empty state container is visible
|
||||
const imgs = document.querySelectorAll('img')
|
||||
expect(imgs).toHaveLength(0)
|
||||
// The empty-state button should exist
|
||||
const uploadButtons = screen.getAllByRole('button')
|
||||
expect(uploadButtons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-003: renders one thumbnail per photo plus one upload tile', () => {
|
||||
const photos = [buildPhoto(), buildPhoto({ id: 2 }), buildPhoto({ id: 3 })]
|
||||
render(<PhotoGallery {...defaultProps} photos={photos} />)
|
||||
const imgs = document.querySelectorAll('img')
|
||||
expect(imgs).toHaveLength(3)
|
||||
// Upload tile button (with Upload icon and "add" text) is present
|
||||
const buttons = screen.getAllByRole('button')
|
||||
// At least the upload tile button exists alongside the header upload button
|
||||
expect(buttons.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-004: clicking thumbnail opens lightbox at correct index', async () => {
|
||||
const user = userEvent.setup()
|
||||
const photos = [buildPhoto(), buildPhoto({ id: 2 })]
|
||||
render(<PhotoGallery {...defaultProps} photos={photos} />)
|
||||
|
||||
const thumbnails = document.querySelectorAll('.aspect-square.rounded-xl.overflow-hidden')
|
||||
expect(thumbnails).toHaveLength(2)
|
||||
await user.click(thumbnails[1] as HTMLElement)
|
||||
|
||||
expect(screen.getByTestId('lightbox')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('lightbox').getAttribute('data-index')).toBe('1')
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-005: closing lightbox hides it', async () => {
|
||||
const user = userEvent.setup()
|
||||
const photos = [buildPhoto()]
|
||||
render(<PhotoGallery {...defaultProps} photos={photos} />)
|
||||
|
||||
const thumbnail = document.querySelector('.aspect-square.rounded-xl.overflow-hidden')
|
||||
await user.click(thumbnail as HTMLElement)
|
||||
expect(screen.getByTestId('lightbox')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByText('close-lightbox'))
|
||||
expect(screen.queryByTestId('lightbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-006: upload button opens upload modal', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<PhotoGallery {...defaultProps} photos={[]} />)
|
||||
|
||||
// The header upload button
|
||||
const uploadButtons = screen.getAllByRole('button')
|
||||
// First button with Upload icon in header
|
||||
await user.click(uploadButtons[0])
|
||||
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('photo-upload')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-007: day filter dropdown shows all days as options', () => {
|
||||
const days = [
|
||||
{ id: 1, day_number: 1, date: '2025-01-10', trip_id: 1, title: null, notes: null, assignments: [], notes_items: [] },
|
||||
{ id: 2, day_number: 2, date: null, trip_id: 1, title: null, notes: null, assignments: [], notes_items: [] },
|
||||
]
|
||||
render(<PhotoGallery {...defaultProps} photos={[]} days={days} />)
|
||||
|
||||
const select = screen.getByRole('combobox')
|
||||
const options = Array.from(select.querySelectorAll('option'))
|
||||
// "All days" + 2 day options
|
||||
expect(options.length).toBe(3)
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-008: filtering by day hides photos from other days', async () => {
|
||||
const user = userEvent.setup()
|
||||
const days = [
|
||||
{ id: 1, day_number: 1, date: null, trip_id: 1, title: null, notes: null, assignments: [], notes_items: [] },
|
||||
{ id: 2, day_number: 2, date: null, trip_id: 1, title: null, notes: null, assignments: [], notes_items: [] },
|
||||
]
|
||||
const photos = [
|
||||
buildPhoto({ id: 1, day_id: 1 }),
|
||||
buildPhoto({ id: 2, day_id: 2 }),
|
||||
]
|
||||
render(<PhotoGallery {...defaultProps} photos={photos} days={days} />)
|
||||
|
||||
const select = screen.getByRole('combobox')
|
||||
await user.selectOptions(select, '1')
|
||||
|
||||
const imgs = document.querySelectorAll('img')
|
||||
expect(imgs).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-009: reset filter button appears and clears filter', async () => {
|
||||
const user = userEvent.setup()
|
||||
const days = [
|
||||
{ id: 1, day_number: 1, date: null, trip_id: 1, title: null, notes: null, assignments: [], notes_items: [] },
|
||||
{ id: 2, day_number: 2, date: null, trip_id: 1, title: null, notes: null, assignments: [], notes_items: [] },
|
||||
]
|
||||
const photos = [
|
||||
buildPhoto({ id: 1, day_id: 1 }),
|
||||
buildPhoto({ id: 2, day_id: 2 }),
|
||||
]
|
||||
render(<PhotoGallery {...defaultProps} photos={photos} days={days} />)
|
||||
|
||||
const select = screen.getByRole('combobox')
|
||||
await user.selectOptions(select, '1')
|
||||
|
||||
// Reset button should now be visible
|
||||
const resetButton = screen.getByRole('button', { name: /reset/i })
|
||||
expect(resetButton).toBeInTheDocument()
|
||||
|
||||
await user.click(resetButton)
|
||||
|
||||
const imgs = document.querySelectorAll('img')
|
||||
expect(imgs).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-010: deleting last photo in lightbox closes lightbox', async () => {
|
||||
const user = userEvent.setup()
|
||||
const photos = [buildPhoto({ id: 1 })]
|
||||
render(<PhotoGallery {...defaultProps} photos={photos} />)
|
||||
|
||||
const thumbnail = document.querySelector('.aspect-square.rounded-xl.overflow-hidden')
|
||||
await user.click(thumbnail as HTMLElement)
|
||||
expect(screen.getByTestId('lightbox')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByText('delete-photo'))
|
||||
|
||||
expect(screen.queryByTestId('lightbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOGALLERY-011: deleting a photo adjusts lightbox index when beyond bounds', async () => {
|
||||
const user = userEvent.setup()
|
||||
const photos = [buildPhoto({ id: 1 }), buildPhoto({ id: 2 })]
|
||||
render(<PhotoGallery {...defaultProps} photos={photos} />)
|
||||
|
||||
const thumbnails = document.querySelectorAll('.aspect-square.rounded-xl.overflow-hidden')
|
||||
await user.click(thumbnails[1] as HTMLElement)
|
||||
|
||||
expect(screen.getByTestId('lightbox').getAttribute('data-index')).toBe('1')
|
||||
|
||||
await user.click(screen.getByText('delete-photo'))
|
||||
|
||||
// Lightbox should still be open but at index 0
|
||||
expect(screen.getByTestId('lightbox')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('lightbox').getAttribute('data-index')).toBe('0')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,194 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { render, screen, fireEvent, waitFor } from '../../../tests/helpers/render'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { resetAllStores } from '../../../tests/helpers/store'
|
||||
import { PhotoLightbox } from './PhotoLightbox'
|
||||
|
||||
const buildPhoto = (overrides = {}) => ({
|
||||
id: 1,
|
||||
url: '/uploads/p1.jpg',
|
||||
caption: null,
|
||||
original_name: 'p1.jpg',
|
||||
day_id: null,
|
||||
place_id: null,
|
||||
file_size: 204800,
|
||||
created_at: '2025-03-10T10:00:00Z',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
photos: [buildPhoto({ id: 1 }), buildPhoto({ id: 2, url: '/uploads/p2.jpg', original_name: 'p2.jpg' })],
|
||||
initialIndex: 0,
|
||||
onClose: vi.fn(),
|
||||
onUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
onDelete: vi.fn().mockResolvedValue(undefined),
|
||||
days: [],
|
||||
places: [],
|
||||
tripId: 99,
|
||||
}
|
||||
|
||||
describe('PhotoLightbox', () => {
|
||||
let confirmSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
resetAllStores()
|
||||
vi.clearAllMocks()
|
||||
confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
confirmSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-001: renders the current photo', () => {
|
||||
render(<PhotoLightbox {...defaultProps} initialIndex={0} />)
|
||||
const img = screen.getByRole('img', { name: /p1\.jpg/i })
|
||||
expect(img).toHaveAttribute('src', '/uploads/p1.jpg')
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-002: shows photo counter "1 / 2"', () => {
|
||||
render(<PhotoLightbox {...defaultProps} initialIndex={0} />)
|
||||
expect(screen.getByText('1 / 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-003: next button advances to second photo', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<PhotoLightbox {...defaultProps} initialIndex={0} />)
|
||||
|
||||
// Find the ChevronRight button — it's the one after the image in the image area
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const nextBtn = buttons.find(btn => btn.querySelector('svg') && btn.className.includes('rounded-full') && btn.className.includes('right-4'))
|
||||
?? buttons.find(btn => btn.className.includes('rounded-full') && !btn.className.includes('left-4'))
|
||||
|
||||
// Use the button with ChevronRight — at index 0, only next button is shown
|
||||
// It's within the image area, has class "rounded-full" and no left-4
|
||||
const imageAreaButtons = buttons.filter(btn => btn.className.includes('rounded-full'))
|
||||
expect(imageAreaButtons).toHaveLength(1) // only next at index 0
|
||||
|
||||
await user.click(imageAreaButtons[0])
|
||||
|
||||
expect(screen.getByText('2 / 2')).toBeInTheDocument()
|
||||
const img = screen.getByRole('img', { name: /p2\.jpg/i })
|
||||
expect(img).toHaveAttribute('src', '/uploads/p2.jpg')
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-004: prev button not shown at index 0', () => {
|
||||
render(<PhotoLightbox {...defaultProps} initialIndex={0} />)
|
||||
// At index 0 only the next (ChevronRight) rounded-full button appears
|
||||
const roundedButtons = screen.getAllByRole('button').filter(btn =>
|
||||
btn.className.includes('rounded-full'),
|
||||
)
|
||||
expect(roundedButtons).toHaveLength(1)
|
||||
// Confirm this single button is the next button (right-4)
|
||||
expect(roundedButtons[0].className).toContain('right-4')
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-005: ArrowRight keyboard event advances photo', () => {
|
||||
render(<PhotoLightbox {...defaultProps} initialIndex={0} />)
|
||||
expect(screen.getByText('1 / 2')).toBeInTheDocument()
|
||||
|
||||
fireEvent.keyDown(window, { key: 'ArrowRight' })
|
||||
|
||||
expect(screen.getByText('2 / 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-006: Escape keyboard event calls onClose', () => {
|
||||
render(<PhotoLightbox {...defaultProps} />)
|
||||
fireEvent.keyDown(window, { key: 'Escape' })
|
||||
expect(defaultProps.onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-007: clicking backdrop calls onClose', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { container } = render(<PhotoLightbox {...defaultProps} />)
|
||||
// The outer div.fixed has the onClick={onClose}. Click it directly.
|
||||
const backdrop = container.firstChild as HTMLElement
|
||||
await user.click(backdrop)
|
||||
expect(defaultProps.onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-008: delete button triggers confirm and calls onDelete', async () => {
|
||||
confirmSpy.mockReturnValue(true)
|
||||
const user = userEvent.setup()
|
||||
render(<PhotoLightbox {...defaultProps} initialIndex={0} />)
|
||||
|
||||
// The trash button has title matching delete
|
||||
const trashBtn = screen.getByTitle(/delete|löschen/i)
|
||||
await user.click(trashBtn)
|
||||
|
||||
expect(confirmSpy).toHaveBeenCalled()
|
||||
expect(defaultProps.onDelete).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-009: delete cancelled via confirm does not call onDelete', async () => {
|
||||
confirmSpy.mockReturnValue(false)
|
||||
const user = userEvent.setup()
|
||||
render(<PhotoLightbox {...defaultProps} initialIndex={0} />)
|
||||
|
||||
const trashBtn = screen.getByTitle(/delete|löschen/i)
|
||||
await user.click(trashBtn)
|
||||
|
||||
expect(confirmSpy).toHaveBeenCalled()
|
||||
expect(defaultProps.onDelete).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-010: clicking caption text enters edit mode', async () => {
|
||||
const user = userEvent.setup()
|
||||
const props = {
|
||||
...defaultProps,
|
||||
photos: [buildPhoto({ id: 1, caption: 'Sunset view' })],
|
||||
}
|
||||
render(<PhotoLightbox {...props} initialIndex={0} />)
|
||||
|
||||
// Click on the caption paragraph
|
||||
const captionEl = screen.getByText('Sunset view')
|
||||
await user.click(captionEl)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue('Sunset view')
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-011: saving caption calls onUpdate', async () => {
|
||||
const user = userEvent.setup()
|
||||
const props = {
|
||||
...defaultProps,
|
||||
photos: [buildPhoto({ id: 1, caption: 'Old caption' })],
|
||||
}
|
||||
render(<PhotoLightbox {...props} initialIndex={0} />)
|
||||
|
||||
// Enter edit mode
|
||||
await user.click(screen.getByText('Old caption'))
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await user.clear(input)
|
||||
await user.type(input, 'New caption')
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onUpdate).toHaveBeenCalledWith(1, { caption: 'New caption' })
|
||||
})
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-012: thumbnail strip renders for multiple photos', () => {
|
||||
const { container } = render(<PhotoLightbox {...defaultProps} initialIndex={0} />)
|
||||
|
||||
// Thumbnail strip has buttons each containing an img with alt=""
|
||||
// querySelectorAll finds them regardless of ARIA role filtering
|
||||
const thumbnailImgs = container.querySelectorAll('button img[alt=""]')
|
||||
expect(thumbnailImgs).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('FE-COMP-PHOTOLIGHTBOX-013: day and place metadata displayed when photo has day/place', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
photos: [buildPhoto({ id: 1, day_id: 1, place_id: 1 })],
|
||||
days: [{ id: 1, day_number: 2, trip_id: 99, date: null, notes: null }],
|
||||
places: [{ id: 1, name: 'Colosseum', trip_id: 99, lat: null, lng: null, category: null, notes: null, day_id: null, address: null, order_index: 0 }],
|
||||
}
|
||||
render(<PhotoLightbox {...props} initialIndex={0} />)
|
||||
|
||||
expect(screen.getByText(/Tag 2/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Colosseum/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,157 @@
|
||||
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('Fotos hier ablegen')).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('Tag verknüpfen')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('Optionale Beschriftung...')).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('Tag verknüpfen')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('Optionale Beschriftung...')).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 Fotos ausgewählt')).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 Fotos ausgewählt')).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 Foto ausgewählt')).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('Optionale Beschriftung...'), '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.getByText(/wird hochgeladen/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(getSubmitButton()).toBeDisabled()
|
||||
|
||||
// Cleanup
|
||||
resolveUpload()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user