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:
jubnl
2026-04-08 21:14:23 +02:00
parent 2b7057b922
commit d4bb8be86b
45 changed files with 13643 additions and 524 deletions
@@ -0,0 +1,147 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '../../../tests/helpers/render'
import { resetAllStores } from '../../../tests/helpers/store'
import { useSettingsStore } from '../../store/settingsStore'
import WeatherWidget from './WeatherWidget'
vi.mock('../../api/client', async (importOriginal) => {
const original = await importOriginal() as any
return {
...original,
weatherApi: {
get: vi.fn(),
},
}
})
// Import after mock so we get the mocked version
import { weatherApi } from '../../api/client'
const buildWeather = (overrides = {}) => ({
temp: 20,
main: 'Clear',
description: 'clear sky',
type: 'forecast',
...overrides,
})
beforeEach(() => {
sessionStorage.clear()
vi.clearAllMocks()
resetAllStores()
})
describe('WeatherWidget', () => {
it('FE-COMP-WEATHERWIDGET-001: renders nothing when lat or lng is null', () => {
const { container } = render(
<WeatherWidget lat={null} lng={null} date="2025-06-01" />
)
expect(container.firstChild).toBeNull()
})
it('FE-COMP-WEATHERWIDGET-002: shows loading indicator while fetching', () => {
vi.mocked(weatherApi.get).mockReturnValue(new Promise(() => {}))
render(<WeatherWidget lat={48.86} lng={2.35} date="2025-06-01" />)
expect(screen.getByText('…')).toBeInTheDocument()
})
it('FE-COMP-WEATHERWIDGET-003: shows error dash when fetch fails', async () => {
vi.mocked(weatherApi.get).mockRejectedValue(new Error('Network error'))
render(<WeatherWidget lat={48.86} lng={2.35} date="2025-06-01" />)
await waitFor(() => {
expect(screen.getByText('—')).toBeInTheDocument()
})
})
it('FE-COMP-WEATHERWIDGET-004: shows error dash when API returns error field', async () => {
vi.mocked(weatherApi.get).mockResolvedValue({ error: 'Not available' })
render(<WeatherWidget lat={48.86} lng={2.35} date="2025-06-01" />)
await waitFor(() => {
expect(screen.getByText('—')).toBeInTheDocument()
})
})
it('FE-COMP-WEATHERWIDGET-005: displays temperature in Celsius', async () => {
vi.mocked(weatherApi.get).mockResolvedValue(buildWeather({ temp: 20 }))
useSettingsStore.setState({ settings: { ...useSettingsStore.getState().settings, temperature_unit: 'celsius' } })
render(<WeatherWidget lat={48.86} lng={2.35} date="2025-06-01" />)
await waitFor(() => {
expect(screen.getByText('20°C')).toBeInTheDocument()
})
})
it('FE-COMP-WEATHERWIDGET-006: converts temperature to Fahrenheit', async () => {
vi.mocked(weatherApi.get).mockResolvedValue(buildWeather({ temp: 20 }))
useSettingsStore.setState({ settings: { ...useSettingsStore.getState().settings, temperature_unit: 'fahrenheit' } })
render(<WeatherWidget lat={48.86} lng={2.35} date="2025-06-01" />)
await waitFor(() => {
expect(screen.getByText('68°F')).toBeInTheDocument()
})
})
it('FE-COMP-WEATHERWIDGET-007: shows "Ø" prefix for climate data', async () => {
vi.mocked(weatherApi.get).mockResolvedValue(buildWeather({ temp: 15, main: 'Clouds', type: 'climate' }))
useSettingsStore.setState({ settings: { ...useSettingsStore.getState().settings, temperature_unit: 'celsius' } })
render(<WeatherWidget lat={48.86} lng={2.35} date="2025-06-01" />)
await waitFor(() => {
expect(screen.getByText(/Ø/)).toBeInTheDocument()
})
})
it('FE-COMP-WEATHERWIDGET-008: compact mode renders inline without description', async () => {
vi.mocked(weatherApi.get).mockResolvedValue(buildWeather({ description: 'clear sky' }))
useSettingsStore.setState({ settings: { ...useSettingsStore.getState().settings, temperature_unit: 'celsius' } })
const { container } = render(
<WeatherWidget lat={48.86} lng={2.35} date="2025-06-01" compact={true} />
)
await waitFor(() => {
expect(screen.getByText('20°C')).toBeInTheDocument()
})
expect(screen.queryByText('clear sky')).not.toBeInTheDocument()
// Outer element should be a span
const tempSpan = screen.getByText('20°C')
expect(tempSpan.closest('span')).toBeInTheDocument()
expect(container.querySelector('div')).toBeNull()
})
it('FE-COMP-WEATHERWIDGET-009: non-compact mode shows description', async () => {
vi.mocked(weatherApi.get).mockResolvedValue(buildWeather({ description: 'clear sky' }))
useSettingsStore.setState({ settings: { ...useSettingsStore.getState().settings, temperature_unit: 'celsius' } })
render(<WeatherWidget lat={48.86} lng={2.35} date="2025-06-01" compact={false} />)
await waitFor(() => {
expect(screen.getByText('clear sky')).toBeInTheDocument()
})
})
it('FE-COMP-WEATHERWIDGET-010: uses cached data from sessionStorage', async () => {
const cached = buildWeather({ temp: 20 })
sessionStorage.setItem('weather_48.86_2.35_2025-06-01', JSON.stringify(cached))
useSettingsStore.setState({ settings: { ...useSettingsStore.getState().settings, temperature_unit: 'celsius' } })
render(<WeatherWidget lat={48.86} lng={2.35} date="2025-06-01" />)
await waitFor(() => {
expect(screen.getByText('20°C')).toBeInTheDocument()
})
expect(weatherApi.get).not.toHaveBeenCalled()
})
it('FE-COMP-WEATHERWIDGET-011: re-fetches in background for cached climate data', async () => {
const climateData = buildWeather({ temp: 15, main: 'Clouds', type: 'climate', description: 'cloudy' })
const forecastData = buildWeather({ temp: 22, main: 'Clear', type: 'forecast', description: 'clear sky' })
sessionStorage.setItem('weather_48.86_2.35_2025-06-01', JSON.stringify(climateData))
vi.mocked(weatherApi.get).mockResolvedValue(forecastData)
useSettingsStore.setState({ settings: { ...useSettingsStore.getState().settings, temperature_unit: 'celsius' } })
render(<WeatherWidget lat={48.86} lng={2.35} date="2025-06-01" />)
// Initially shows climate data
await waitFor(() => {
expect(screen.getByText(/Ø/)).toBeInTheDocument()
})
// After background fetch resolves, shows forecast data
await waitFor(() => {
expect(screen.getByText('22°C')).toBeInTheDocument()
})
expect(screen.queryByText(/Ø/)).not.toBeInTheDocument()
})
})