Raise frontend branch coverage from 86.84% to 93.78%

Branches were the weakest metric and had the least headroom above the
85% floor. These are tests for decision paths that were genuinely
unexercised, not padding.

The largest gaps were systemic rather than per-file: the
`err instanceof Error ? ... : fallback` branch was untested across eight
data hooks, useVacationDays had no dedicated test file at all, and
Calendar.tsx's missing-workspace/child/personalData fallbacks were never
hit.

Left deliberately uncovered, with reasons: `?? fallback` guards whose
fallback cannot occur given how the value is constructed upstream, and
guards behind buttons that are disabled whenever the guard would matter.
Reaching those means calling handlers directly, which tests the test
rather than the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
2026-09-14 18:54:56 +02:00
parent 8869ea8e55
commit c5481a05f0
23 changed files with 1063 additions and 5 deletions
@@ -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(<RedDaysList {...defaultProps} />)
// 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(<RedDaysList {...defaultProps} days={workingOverrideDays} />)
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(<RedDaysList {...defaultProps} days={workingDays} />)
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(<RedDaysList {...defaultProps} startYear={2026} minYear={2026} />)
expect(screen.getByTestId('red-days-prev')).toBeDisabled()
@@ -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(
<CalendarDay date="2026-06-15" onClick={onClick} onActivate={onActivate} />
)
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(
@@ -65,6 +65,22 @@ describe('CalendarEvent', () => {
expect(stopPropagation).toHaveBeenCalled()
})
it('prefixes the label with the parent label when provided', () => {
const { getByTestId } = render(
<CalendarEvent leaveType="vacation" color="#FF5733" parentLabel="Alice" />
)
const event = getByTestId('calendar-event')
expect(event.textContent).toBe('Alice · vacation')
})
it('shows the double-day badge when isDubbeldag is true', () => {
const { getByTestId } = render(
<CalendarEvent leaveType="vacation" color="#FF5733" isDubbeldag={true} />
)
const event = getByTestId('calendar-event')
expect(event.textContent).toContain('2×')
})
it('provides accessibility label', () => {
const { getByTestId } = render(
<CalendarEvent leaveType="vacation" color="#FF5733" onDismiss={() => {}} />
@@ -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(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
const events = screen.getAllByTestId('calendar-event')
expect(events.length).toBeGreaterThan(0)
})
it('handles empty planned days', () => {
render(<CalendarWeek {...defaultProps} plannedDays={[]} />)
expect(screen.queryByTestId('calendar-event')).not.toBeInTheDocument()
@@ -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(
<DaysBreakdown breakdown={undefined as unknown as DaysBreakdownProps['breakdown']} />
)
expect(getByTestId('days-breakdown').textContent).toContain('Inga dagar registrerade än')
})
it('renders table with breakdown data', () => {
const breakdown = [
{ type: 'vacation', count: 10 },
@@ -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(<ParentalLeaveBreakdown data={dataWithoutTiers} />)
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(<ParentalLeaveBreakdown data={mockData} />)
expect(screen.getByTestId('parental-leave-breakdown')).toBeInTheDocument()
@@ -92,4 +102,26 @@ describe('ParentalLeaveBreakdown', () => {
render(<ParentalLeaveBreakdown data={mockData} />)
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(
<LanguageProvider>
<ParentalLeaveBreakdown data={mockData} />
</LanguageProvider>,
)
// 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()
}
})
})
@@ -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(
<DayOverridesInput
overrides={[]}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
// 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(
<DayOverridesInput
overrides={mockOverrides}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
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', () => {
+44 -1
View File
@@ -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(
<MemoryRouter initialEntries={[path]}>
{element}
</MemoryRouter>,
)
}
describe('Header', () => {
it('renders header with default title', () => {
renderWithRouter(<Header />)
@@ -75,4 +83,39 @@ describe('Header', () => {
renderWithRouter(<Header />)
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(<Header childrenList={childrenList} />)
const select = screen.getByTestId('child-select') as HTMLSelectElement
// selectedChildId ?? '' binds the select to a value ('') that matches no
// <option>, so the browser falls back to the first option — it must not
// be coerced to something else (e.g. "undefined" or a stale selection).
expect(select.value).toBe('1')
})
it('marks the calendar link active on the calendar route', () => {
renderAtPath(<Header />, '/calendar')
expect(screen.getByText('Kalender')).toHaveClass('header__nav-link--active')
expect(screen.getByText('Översikt')).not.toHaveClass('header__nav-link--active')
})
it('marks the plan link active on the plan route', () => {
renderAtPath(<Header />, '/plan')
expect(screen.getByText('Planering')).toHaveClass('header__nav-link--active')
})
it('marks the tips link active on the tips route', () => {
renderAtPath(<Header />, '/tips')
expect(screen.getByText('Tips')).toHaveClass('header__nav-link--active')
})
it('marks the settings link active on the settings route, and the overview link inactive', () => {
renderAtPath(<Header />, '/settings')
expect(screen.getByText('Inställningar')).toHaveClass('header__nav-link--active')
expect(screen.getByText('Översikt')).not.toHaveClass('header__nav-link--active')
})
})
@@ -463,6 +463,38 @@ describe('PlannedDayModal', () => {
})
})
it('displays a generic error message for non-Error thrown in delete', async () => {
const existingDay: PlannedLeaveDay = {
id: 'day-1',
workspaceId: 'ws-1',
childId: 'c-1',
parentPersonalDataUuid: 'p1',
date: '2026-06-15',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
}
mockOnDelete.mockRejectedValue('Some error')
vi.spyOn(window, 'confirm').mockReturnValue(true)
render(
<PlannedDayModal
{...defaultProps}
isOpen={true}
existingDay={existingDay}
onDelete={mockOnDelete}
/>
)
const deleteButton = screen.getByTestId('delete-button')
await userEvent.click(deleteButton)
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Det gick inte att ta bort den planerade dagen')
})
})
it('clears local error when save starts after a failed attempt', async () => {
// First save fails
mockOnSave.mockRejectedValueOnce(new Error('First save failed'))