diff --git a/frontend/src/components/calendar/RedDaysList.test.tsx b/frontend/src/components/calendar/RedDaysList.test.tsx index 36fd9fe..e59a73f 100644 --- a/frontend/src/components/calendar/RedDaysList.test.tsx +++ b/frontend/src/components/calendar/RedDaysList.test.tsx @@ -36,6 +36,14 @@ const baseDays: CalendarDayEntry[] = [ overrideIsWorkingDay: null, isFree: true, }, + { + date: '2026-05-04', + name: null, + dayType: 'working_day', + isOverride: false, + overrideIsWorkingDay: null, + isFree: true, + }, ] describe('RedDaysList', () => { @@ -80,6 +88,47 @@ describe('RedDaysList', () => { expect(year2028).toHaveTextContent(/Inga röda dagar eller undantag/) }) + it('skips a plain working day that is neither a red day nor an override', () => { + render() + // 2026-05-04 is a plain working_day, non-override entry in baseDays — it + // must not show up as a list item under its year. + expect(screen.queryByTestId('red-days-item-2026-05-04')).not.toBeInTheDocument() + }) + + it('labels a working-day override distinctly from a free-day override', () => { + const workingOverrideDays = [ + { + date: '2026-03-02', + name: null, + dayType: 'working_day' as const, + isOverride: true, + overrideIsWorkingDay: true, + isFree: false, + }, + ] + render() + const badge = screen.getByTestId('red-days-override-badge-2026-03-02') + expect(badge).toHaveClass('red-days-list__override-badge--working') + expect(badge).toHaveTextContent('Undantag: arbetsdag') + }) + + it('shows the effective-working label for a day that is not free', () => { + const workingDays = [ + { + date: '2026-03-02', + name: null, + dayType: 'working_day' as const, + isOverride: true, + overrideIsWorkingDay: true, + isFree: false, + }, + ] + render() + const item = screen.getByTestId('red-days-item-2026-03-02') + expect(item.querySelector('.red-days-list__effective--working')).toBeInTheDocument() + expect(item.querySelector('.red-days-list__effective--free')).not.toBeInTheDocument() + }) + it('disables the previous button at minYear', () => { render() expect(screen.getByTestId('red-days-prev')).toBeDisabled() diff --git a/frontend/src/components/common/CalendarDay.test.tsx b/frontend/src/components/common/CalendarDay.test.tsx index 3572812..b58ae1a 100644 --- a/frontend/src/components/common/CalendarDay.test.tsx +++ b/frontend/src/components/common/CalendarDay.test.tsx @@ -64,6 +64,18 @@ describe('CalendarDay', () => { expect(onClick).toHaveBeenCalledTimes(1) }) + it('calls onActivate instead of onClick on Enter when onActivate is provided', () => { + const onClick = vi.fn() + const onActivate = vi.fn() + const { getByTestId } = render( + + ) + const day = getByTestId('calendar-day') + fireEvent.keyDown(day, { key: 'Enter' }) + expect(onActivate).toHaveBeenCalledTimes(1) + expect(onClick).not.toHaveBeenCalled() + }) + it('calls onClick on Space key', () => { const onClick = vi.fn() const { getByTestId } = render( diff --git a/frontend/src/components/common/CalendarEvent.test.tsx b/frontend/src/components/common/CalendarEvent.test.tsx index e2aeec8..a416722 100644 --- a/frontend/src/components/common/CalendarEvent.test.tsx +++ b/frontend/src/components/common/CalendarEvent.test.tsx @@ -65,6 +65,22 @@ describe('CalendarEvent', () => { expect(stopPropagation).toHaveBeenCalled() }) + it('prefixes the label with the parent label when provided', () => { + const { getByTestId } = render( + + ) + const event = getByTestId('calendar-event') + expect(event.textContent).toBe('Alice · vacation') + }) + + it('shows the double-day badge when isDubbeldag is true', () => { + const { getByTestId } = render( + + ) + const event = getByTestId('calendar-event') + expect(event.textContent).toContain('2×') + }) + it('provides accessibility label', () => { const { getByTestId } = render( {}} /> diff --git a/frontend/src/components/common/CalendarWeek.test.tsx b/frontend/src/components/common/CalendarWeek.test.tsx index 39aa076..c870f5a 100644 --- a/frontend/src/components/common/CalendarWeek.test.tsx +++ b/frontend/src/components/common/CalendarWeek.test.tsx @@ -412,6 +412,39 @@ describe('CalendarWeek', () => { expect(events.length).toBeGreaterThanOrEqual(2) }) + it('closes an open bar that runs through the last day of the week', () => { + // A run of planned days ending on the week's final date (2026-01-11) + // leaves the bar "open" when the loop over weekDates finishes, which + // exercises the post-loop flush rather than the mid-loop gap-close path. + const plannedDays: PlannedLeaveDay[] = [ + { + id: '1', + workspaceId: 'ws-1', + childId: 'child-1', + parentPersonalDataUuid: 'parent-1', + date: '2026-01-10', + dayType: 'vacation', + isDubbeldag: false, + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + }, + { + id: '2', + workspaceId: 'ws-1', + childId: 'child-1', + parentPersonalDataUuid: 'parent-1', + date: '2026-01-11', + dayType: 'vacation', + isDubbeldag: false, + createdAt: '2026-01-01', + updatedAt: '2026-01-01', + }, + ] + render() + const events = screen.getAllByTestId('calendar-event') + expect(events.length).toBeGreaterThan(0) + }) + it('handles empty planned days', () => { render() expect(screen.queryByTestId('calendar-event')).not.toBeInTheDocument() diff --git a/frontend/src/components/dashboard/DaysBreakdown.test.tsx b/frontend/src/components/dashboard/DaysBreakdown.test.tsx index 7bcd257..0f64bfa 100644 --- a/frontend/src/components/dashboard/DaysBreakdown.test.tsx +++ b/frontend/src/components/dashboard/DaysBreakdown.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { render } from '@testing-library/react' -import { DaysBreakdown } from './DaysBreakdown' +import { DaysBreakdown, type DaysBreakdownProps } from './DaysBreakdown' describe('DaysBreakdown', () => { it('renders empty state when no breakdown provided', () => { @@ -8,6 +8,13 @@ describe('DaysBreakdown', () => { expect(getByTestId('days-breakdown').textContent).toContain('Inga dagar registrerade än') }) + it('renders empty state when breakdown itself is missing (defensive null/undefined guard)', () => { + const { getByTestId } = render( + + ) + expect(getByTestId('days-breakdown').textContent).toContain('Inga dagar registrerade än') + }) + it('renders table with breakdown data', () => { const breakdown = [ { type: 'vacation', count: 10 }, diff --git a/frontend/src/components/dashboard/ParentalLeaveBreakdown.test.tsx b/frontend/src/components/dashboard/ParentalLeaveBreakdown.test.tsx index de7e470..e98d30e 100644 --- a/frontend/src/components/dashboard/ParentalLeaveBreakdown.test.tsx +++ b/frontend/src/components/dashboard/ParentalLeaveBreakdown.test.tsx @@ -1,7 +1,8 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { render, screen } from '@testing-library/react' import { ParentalLeaveBreakdown } from './ParentalLeaveBreakdown' import { LEAVE_RULES } from '@/utils/constants' +import { LanguageProvider } from '@/i18n' import type { ParentalLeaveFetch } from '@/types' describe('ParentalLeaveBreakdown', () => { @@ -36,6 +37,15 @@ describe('ParentalLeaveBreakdown', () => { expect(screen.getByText(/Ingen data hämtad ännu/)).toBeInTheDocument() }) + it('omits the tier rows when tierData is absent', () => { + const dataWithoutTiers = { ...mockData, tierData: undefined } as unknown as ParentalLeaveFetch + render() + expect(screen.queryByTestId('breakdown-sjukpenning')).not.toBeInTheDocument() + expect(screen.queryByTestId('breakdown-lagsta-niva')).not.toBeInTheDocument() + // The rows that don't depend on tierData still render. + expect(screen.getByTestId('breakdown-transferable')).toBeInTheDocument() + }) + it('renders breakdown table with data', () => { render() expect(screen.getByTestId('parental-leave-breakdown')).toBeInTheDocument() @@ -92,4 +102,26 @@ describe('ParentalLeaveBreakdown', () => { render() expect(screen.getByText(/2026-06-13/)).toBeInTheDocument() }) + + it('formats the provenance date using the en-US locale when the language is English', () => { + Object.defineProperty(window.navigator, 'language', { value: 'en-US', configurable: true }) + vi.stubGlobal('localStorage', { + getItem: () => null, + setItem: () => undefined, + removeItem: () => undefined, + }) + + try { + render( + + + , + ) + // en-US formats as M/D/YYYY, distinct from the sv-SE "2026-06-13" form. + expect(screen.getByText(/6\/13\/2026/)).toBeInTheDocument() + } finally { + Object.defineProperty(window.navigator, 'language', { value: 'sv-SE', configurable: true }) + vi.unstubAllGlobals() + } + }) }) diff --git a/frontend/src/components/inputs/DayOverridesInput.test.tsx b/frontend/src/components/inputs/DayOverridesInput.test.tsx index cb195d8..2f9d165 100644 --- a/frontend/src/components/inputs/DayOverridesInput.test.tsx +++ b/frontend/src/components/inputs/DayOverridesInput.test.tsx @@ -203,6 +203,25 @@ describe('DayOverridesInput', () => { const submitButton = screen.getByTestId('add-override-button') as HTMLButtonElement expect(submitButton.disabled).toBe(true) }) + + it('does not call onAdd when the form is submitted with no date entered', async () => { + render( + + ) + + // Submit the form directly (bypassing the disabled submit button) to + // exercise the handler's own empty-date guard. + fireEvent.submit(screen.getByTestId('add-override-form')) + + await waitFor(() => { + expect(mockOnAdd).not.toHaveBeenCalled() + }) + }) }) describe('removing override', () => { @@ -222,6 +241,26 @@ describe('DayOverridesInput', () => { expect(mockOnRemove).toHaveBeenCalledWith('do-1') }) }) + + it('swallows an onRemove failure instead of throwing (handled by the parent)', async () => { + mockOnRemove.mockRejectedValueOnce(new Error('delete failed')) + render( + + ) + + fireEvent.click(screen.getByTestId('remove-override-do-1')) + + await waitFor(() => { + expect(mockOnRemove).toHaveBeenCalledWith('do-1') + }) + // The item is still rendered — the component didn't crash on the rejection. + expect(screen.getByTestId('override-item-do-1')).toBeInTheDocument() + }) }) describe('loading state', () => { diff --git a/frontend/src/components/layout/Header.test.tsx b/frontend/src/components/layout/Header.test.tsx index 831257c..63d4d3d 100644 --- a/frontend/src/components/layout/Header.test.tsx +++ b/frontend/src/components/layout/Header.test.tsx @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from 'vitest' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { BrowserRouter } from 'react-router-dom' +import { BrowserRouter, MemoryRouter } from 'react-router-dom' import { Header } from './Header' const renderWithRouter = (element: React.ReactElement) => { @@ -12,6 +12,14 @@ const renderWithRouter = (element: React.ReactElement) => { ) } +const renderAtPath = (element: React.ReactElement, path: string) => { + return render( + + {element} + , + ) +} + describe('Header', () => { it('renders header with default title', () => { renderWithRouter(
) @@ -75,4 +83,39 @@ describe('Header', () => { renderWithRouter(
) expect(screen.queryByTestId('logout-button')).not.toBeInTheDocument() }) + + it('does not crash and renders the select when selectedChildId is not provided (falls back to "")', () => { + const childrenList = [ + { id: '1', name: 'Emma' }, + { id: '2', name: 'Oscar' }, + ] + renderWithRouter(
) + const select = screen.getByTestId('child-select') as HTMLSelectElement + // selectedChildId ?? '' binds the select to a value ('') that matches no + //