Files
F-r-ldraledigplannerare/frontend/src/components/common/CalendarWeek.test.tsx
T
jacobh2 fe120ee222 Remove every as any from the test suite and enforce it
ESLint turned `no-explicit-any` off for test files, and 219 casts had
accumulated across 12 of them. Two shapes, very different risks: most
were `(useWorkspace as any).mockReturnValue(...)`, mere laziness that
`vi.mocked()` does properly; the rest cast partial fixtures onto API
types, which erases the contract exactly where a test should enforce it.

That second kind matters here specifically. This project's recurring bug
is a field silently dropped between backend and frontend -- the
hand-written mapping in models/plan.py has done it twice, and apiFetch
mishandling structured errors was the same shape. A test that casts an
incomplete fixture to `any` keeps passing through precisely that change.

A typed fixture layer already existed at tests/fixtures/mockData.ts and
was imported by exactly zero tests: the right tool was there and
invisible, so everyone reached for a cast. It is now a set of
`make*(overrides: Partial<T>) => T` factories returning complete, valid
objects, split by domain with a barrel. When a type gains a field, one
factory fails to compile instead of thirty tests silently passing.

With the count at zero the ESLint exemption is deleted, so this cannot
regrow. Removing it immediately caught two `}: any` prop stubs that a
grep for "as any" had missed; those mocks now use the real components'
exported prop types.

`as unknown as T` survives in five places, each a test deliberately
feeding invalid input (an undefined breakdown, missing tierData, a bogus
leave type). CLAUDE.md now documents that as the only accepted escape,
and points at the factories.

interface PollResponse in useScraperFetch.ts is exported so its shape can
be derived rather than duplicated -- pollScraperSession declares
Promise<unknown>, so there was no other honest way to type it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-15 04:17:22 +02:00

925 lines
31 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { CalendarWeek } from './CalendarWeek'
import type { PlannedLeaveDay } from '@/types'
import type { CalendarDayProps } from './CalendarDay'
import type { CalendarEventProps } from './CalendarEvent'
vi.mock('@/i18n', () => ({
useTranslation: () => ({
t: (key: string, _data?: Record<string, unknown>) => {
const translations: Record<string, string> = {
'calendar.partner': 'Partner',
'leaveType.vacation': 'Semester',
'leaveType.sjukpenning': 'Sjukpenning',
'leaveType.lagsta': 'Lägsta nivå',
}
return translations[key] ?? key
},
}),
}))
vi.mock('./CalendarDay', () => ({
CalendarDay: ({
date,
isSelected,
isCurrentMonth,
isToday,
onClick,
onPointerDown,
onPointerEnter,
dayType,
holidayName,
isOverride,
overrideIsWorkingDay,
}: CalendarDayProps) => (
<div
data-testid={`calendar-day-${date}`}
className={`${isSelected ? 'selected' : ''} ${isCurrentMonth ? 'current-month' : ''} ${isToday ? 'today' : ''}`}
data-day-type={dayType ?? ''}
data-holiday-name={holidayName ?? ''}
data-is-override={isOverride ? 'true' : 'false'}
data-override-is-working-day={overrideIsWorkingDay === null || overrideIsWorkingDay === undefined ? '' : String(overrideIsWorkingDay)}
onClick={onClick}
onPointerDown={() => onPointerDown?.(date)}
onPointerEnter={() => onPointerEnter?.(date)}
>
{date}
</div>
),
}))
vi.mock('./CalendarEvent', () => ({
CalendarEvent: ({ leaveType, isDubbeldag, parentLabel }: CalendarEventProps) => (
<div data-testid="calendar-event" className={isDubbeldag ? 'dubbeldag' : ''}>
{parentLabel} · {leaveType}
</div>
),
}))
describe('CalendarWeek', () => {
const defaultProps = {
weekDates: ['2026-01-05', '2026-01-06', '2026-01-07', '2026-01-08', '2026-01-09', '2026-01-10', '2026-01-11'],
plannedDays: [],
selectedDates: new Set<string>(),
parentName: 'Test Parent',
personalDataUuid: 'parent-1',
isCurrentMonth: true,
showWeekNumbers: false,
onDayClick: vi.fn(),
onPointerDown: vi.fn(),
onPointerEnter: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('week number display', () => {
it('shows week number when showWeekNumbers is true', () => {
render(<CalendarWeek {...defaultProps} showWeekNumbers={true} />)
// Week containing 2026-01-05 is week 2
expect(screen.getByText(/v\.\d+/)).toBeInTheDocument()
})
it('hides week number when showWeekNumbers is false', () => {
render(<CalendarWeek {...defaultProps} showWeekNumbers={false} />)
const weekNumberElement = screen.queryByText(/^v\.\d+$/)
// The week number is rendered only inside a specific class; when showWeekNumbers is false,
// the conditional doesn't render it
expect(weekNumberElement).not.toBeInTheDocument()
})
it('displays correct week number for a given week', () => {
// 2026-01-05 is a Monday in week 2
render(<CalendarWeek {...defaultProps} weekDates={defaultProps.weekDates} showWeekNumbers={true} />)
const weekNumberText = screen.getByText('v.2')
expect(weekNumberText).toBeInTheDocument()
})
})
describe('day rendering', () => {
it('renders all 7 days of the week', () => {
render(<CalendarWeek {...defaultProps} />)
defaultProps.weekDates.forEach((date) => {
expect(screen.getByTestId(`calendar-day-${date}`)).toBeInTheDocument()
})
})
it('renders each day with correct date string', () => {
render(<CalendarWeek {...defaultProps} />)
defaultProps.weekDates.forEach((date) => {
expect(screen.getByText(date)).toBeInTheDocument()
})
})
it('marks all days as current month when isCurrentMonth is true', () => {
render(<CalendarWeek {...defaultProps} isCurrentMonth={true} />)
defaultProps.weekDates.forEach((date) => {
const dayElement = screen.getByTestId(`calendar-day-${date}`)
expect(dayElement).toHaveClass('current-month')
})
})
it('marks days outside the current month with current-month class set to false', () => {
// When a week spans month boundaries, days are marked based on their month vs the first day's month
// 2025-12-29 is first, so 12/29-31 are marked as current (Dec), 01/01-04 are marked as outside (Jan)
const prevMonthDates = ['2025-12-29', '2025-12-30', '2025-12-31', '2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04']
render(
<CalendarWeek
{...defaultProps}
weekDates={prevMonthDates}
isCurrentMonth={true}
/>,
)
// Days from previous month (Dec 2025) should have current-month class (first date determines the month)
const decDayElement = screen.getByTestId('calendar-day-2025-12-29')
expect(decDayElement).toHaveClass('current-month')
// Days from next month should NOT have current-month class
const janDayElement = screen.getByTestId('calendar-day-2026-01-01')
expect(janDayElement).not.toHaveClass('current-month')
})
it('highlights today when date matches current date', () => {
const today = new Date()
const todayString = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
const weekDates = [
'2026-01-05',
'2026-01-06',
'2026-01-07',
'2026-01-08',
'2026-01-09',
'2026-01-10',
'2026-01-11',
]
// Only test if today happens to fall in the week we're rendering
if (weekDates.includes(todayString)) {
render(
<CalendarWeek
{...defaultProps}
weekDates={weekDates}
/>,
)
const todayElement = screen.getByTestId(`calendar-day-${todayString}`)
expect(todayElement).toHaveClass('today')
}
})
})
describe('selected dates', () => {
it('marks selected dates in the week', () => {
const selectedDates = new Set(['2026-01-06', '2026-01-07'])
render(<CalendarWeek {...defaultProps} selectedDates={selectedDates} />)
expect(screen.getByTestId('calendar-day-2026-01-06')).toHaveClass('selected')
expect(screen.getByTestId('calendar-day-2026-01-07')).toHaveClass('selected')
})
it('does not mark unselected dates', () => {
const selectedDates = new Set(['2026-01-06'])
render(<CalendarWeek {...defaultProps} selectedDates={selectedDates} />)
expect(screen.getByTestId('calendar-day-2026-01-05')).not.toHaveClass('selected')
})
it('handles empty selection set', () => {
render(<CalendarWeek {...defaultProps} selectedDates={new Set()} />)
defaultProps.weekDates.forEach((date) => {
expect(screen.getByTestId(`calendar-day-${date}`)).not.toHaveClass('selected')
})
})
})
describe('planned days and bars', () => {
it('renders bars for planned days in the week', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
expect(screen.getByTestId('calendar-event')).toBeInTheDocument()
})
it('does not render bars for planned days outside the week', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-02-05',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
expect(screen.queryByTestId('calendar-event')).not.toBeInTheDocument()
})
it('renders parent name for own planned days', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} parentName="Alice" />)
expect(screen.getByText(/Alice · Semester/)).toBeInTheDocument()
})
it('renders "Partner" label for partner planned days', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'other-parent',
date: '2026-01-05',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
expect(screen.getByText(/Partner · Semester/)).toBeInTheDocument()
})
it('renders different day types with localized labels', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
dayType: 'sjukpenning',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
{
id: '2',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-06',
dayType: 'lagsta',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
expect(screen.getByText(/Sjukpenning/)).toBeInTheDocument()
expect(screen.getByText(/Lägsta nivå/)).toBeInTheDocument()
})
it('marks dubbeldag days with dubbeldag class', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
dayType: 'vacation',
isDubbeldag: true,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
const eventElement = screen.getByTestId('calendar-event')
expect(eventElement).toHaveClass('dubbeldag')
})
it('renders continuous bars spanning multiple days', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
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-06',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
{
id: '3',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-07',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
// Should render at least one bar for the continuous period
const events = screen.getAllByTestId('calendar-event')
expect(events.length).toBeGreaterThan(0)
})
it('renders separate bars for different day types on the same day', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
{
id: '2',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'other-parent',
date: '2026-01-05',
dayType: 'sjukpenning',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
const events = screen.getAllByTestId('calendar-event')
// Should have separate bars for the two different (parent, dayType) combinations
expect(events.length).toBeGreaterThanOrEqual(2)
})
it('renders separate bars for same day type but different dubbeldag flag', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
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-05',
dayType: 'vacation',
isDubbeldag: true,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
const events = screen.getAllByTestId('calendar-event')
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()
})
})
describe('event handlers', () => {
it('calls onDayClick when clicking a day in current month', () => {
render(<CalendarWeek {...defaultProps} isCurrentMonth={true} />)
const dayElement = screen.getByTestId('calendar-day-2026-01-05')
fireEvent.click(dayElement)
expect(defaultProps.onDayClick).toHaveBeenCalledWith('2026-01-05')
})
it('does not call onDayClick for days outside the week\'s reference month', () => {
// When a week spans month boundaries, only days in the same month as the first date
// will trigger handlers. 2025-12-29 is first (Dec), so 01/01+ will not trigger handlers.
const weekDatesWithPrevMonth = ['2025-12-29', '2025-12-30', '2025-12-31', '2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04']
const onDayClick = vi.fn()
render(
<CalendarWeek
{...defaultProps}
weekDates={weekDatesWithPrevMonth}
isCurrentMonth={true}
onDayClick={onDayClick}
/>,
)
// Click a day outside the reference month (January when December is reference)
const janDayElement = screen.getByTestId('calendar-day-2026-01-01')
fireEvent.click(janDayElement)
expect(onDayClick).not.toHaveBeenCalled()
})
it('does not call onDayClick when isCurrentMonth is false', () => {
const onDayClick = vi.fn()
render(
<CalendarWeek
{...defaultProps}
isCurrentMonth={false}
onDayClick={onDayClick}
/>,
)
const dayElement = screen.getByTestId('calendar-day-2026-01-05')
fireEvent.click(dayElement)
expect(onDayClick).not.toHaveBeenCalled()
})
it('calls onPointerDown when starting drag on a current-month day', () => {
render(<CalendarWeek {...defaultProps} isCurrentMonth={true} />)
const dayElement = screen.getByTestId('calendar-day-2026-01-05')
fireEvent.pointerDown(dayElement)
expect(defaultProps.onPointerDown).toHaveBeenCalledWith('2026-01-05')
})
it('does not call onPointerDown for days outside the week\'s reference month', () => {
const weekDatesWithPrevMonth = ['2025-12-29', '2025-12-30', '2025-12-31', '2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04']
const onPointerDown = vi.fn()
render(
<CalendarWeek
{...defaultProps}
weekDates={weekDatesWithPrevMonth}
isCurrentMonth={true}
onPointerDown={onPointerDown}
/>,
)
const janDayElement = screen.getByTestId('calendar-day-2026-01-01')
fireEvent.pointerDown(janDayElement)
expect(onPointerDown).not.toHaveBeenCalled()
})
it('calls onPointerEnter when dragging through a current-month day', () => {
render(<CalendarWeek {...defaultProps} isCurrentMonth={true} />)
const dayElement = screen.getByTestId('calendar-day-2026-01-06')
fireEvent.pointerEnter(dayElement)
expect(defaultProps.onPointerEnter).toHaveBeenCalledWith('2026-01-06')
})
it('does not call onPointerEnter for days outside the week\'s reference month', () => {
const weekDatesWithPrevMonth = ['2025-12-29', '2025-12-30', '2025-12-31', '2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04']
const onPointerEnter = vi.fn()
render(
<CalendarWeek
{...defaultProps}
weekDates={weekDatesWithPrevMonth}
isCurrentMonth={true}
onPointerEnter={onPointerEnter}
/>,
)
const janDayElement = screen.getByTestId('calendar-day-2026-01-01')
fireEvent.pointerEnter(janDayElement)
expect(onPointerEnter).not.toHaveBeenCalled()
})
it('does not call event handlers when isCurrentMonth is false', () => {
const onDayClick = vi.fn()
const onPointerDown = vi.fn()
const onPointerEnter = vi.fn()
render(
<CalendarWeek
{...defaultProps}
isCurrentMonth={false}
onDayClick={onDayClick}
onPointerDown={onPointerDown}
onPointerEnter={onPointerEnter}
/>,
)
const dayElement = screen.getByTestId('calendar-day-2026-01-05')
fireEvent.click(dayElement)
fireEvent.pointerDown(dayElement)
fireEvent.pointerEnter(dayElement)
expect(onDayClick).not.toHaveBeenCalled()
expect(onPointerDown).not.toHaveBeenCalled()
expect(onPointerEnter).not.toHaveBeenCalled()
})
})
describe('complex scenarios', () => {
it('handles a week with mixed planned days from both parents', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
{
id: '2',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-2',
date: '2026-01-05',
dayType: 'sjukpenning',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
{
id: '3',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-06',
dayType: 'vacation',
isDubbeldag: true,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(
<CalendarWeek
{...defaultProps}
plannedDays={plannedDays}
parentName="Alice"
/>,
)
expect(screen.getAllByText(/Alice · Semester/)).toHaveLength(2)
expect(screen.getByText(/Partner · Sjukpenning/)).toBeInTheDocument()
const dubbeldagElements = screen.getAllByTestId('calendar-event').filter((el) => el.classList.contains('dubbeldag'))
expect(dubbeldagElements.length).toBeGreaterThan(0)
})
it('handles broken bar rendering with gaps in consecutive days', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
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-06',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
// Gap on 2026-01-07
{
id: '3',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-08',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
const events = screen.getAllByTestId('calendar-event')
// Should have at least 2 bars: one for 05-06 and one for 08
expect(events.length).toBeGreaterThanOrEqual(2)
})
it('renders all day types with correct colors', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
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-06',
dayType: 'sjukpenning',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
{
id: '3',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-07',
dayType: 'lagsta',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
const events = screen.getAllByTestId('calendar-event')
expect(events.length).toBeGreaterThanOrEqual(3)
})
it('filters bars to only show those spanning the current day cell', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
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-06',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} />)
// Should render bars for both 2026-01-05 and 2026-01-06
// The bars represent a continuous period, so they should be visible
const events = screen.getAllByTestId('calendar-event')
expect(events.length).toBeGreaterThan(0)
})
})
describe('month boundary edge cases', () => {
it('correctly identifies days at month boundaries', () => {
// Jan 31 and Feb 1 in the same week
const weekDates = ['2026-01-26', '2026-01-27', '2026-01-28', '2026-01-29', '2026-01-30', '2026-01-31', '2026-02-01']
render(
<CalendarWeek
{...defaultProps}
weekDates={weekDates}
isCurrentMonth={true}
/>,
)
// Jan 31 (index 5) should be in current month
const jan31 = screen.getByTestId('calendar-day-2026-01-31')
expect(jan31).toHaveClass('current-month')
// Feb 1 (index 6) should NOT be in current month (isCurrentMonth only marks first week's month ref)
const feb1 = screen.getByTestId('calendar-day-2026-02-01')
expect(feb1).not.toHaveClass('current-month')
})
it('handles start of month week', () => {
const weekDates = ['2026-01-26', '2026-01-27', '2026-01-28', '2026-01-29', '2026-01-30', '2026-01-31', '2026-02-01']
render(
<CalendarWeek
{...defaultProps}
weekDates={weekDates}
isCurrentMonth={true}
/>,
)
// All dates should render
weekDates.forEach((date) => {
expect(screen.getByTestId(`calendar-day-${date}`)).toBeInTheDocument()
})
})
it('handles end of month week', () => {
const weekDates = ['2025-12-29', '2025-12-30', '2025-12-31', '2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04']
render(
<CalendarWeek
{...defaultProps}
weekDates={weekDates}
isCurrentMonth={true}
/>,
)
// All dates should render
weekDates.forEach((date) => {
expect(screen.getByTestId(`calendar-day-${date}`)).toBeInTheDocument()
})
})
})
describe('memoization and performance', () => {
it('memoizes bar computation based on weekDates and plannedDays', () => {
const plannedDays: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
const { rerender } = render(
<CalendarWeek
{...defaultProps}
plannedDays={plannedDays}
parentName="Alice"
/>,
)
expect(screen.getByText(/Alice · Semester/)).toBeInTheDocument()
// Change unrelated prop (onDayClick) - bars should remain the same
const newOnDayClick = vi.fn()
rerender(
<CalendarWeek
{...defaultProps}
plannedDays={plannedDays}
onDayClick={newOnDayClick}
parentName="Alice"
/>,
)
expect(screen.getByText(/Alice · Semester/)).toBeInTheDocument()
})
it('recomputes bars when plannedDays change', () => {
const plannedDays1: PlannedLeaveDay[] = [
{
id: '1',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-05',
dayType: 'vacation',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
const plannedDays2: PlannedLeaveDay[] = [
...plannedDays1,
{
id: '2',
workspaceId: 'ws-1',
childId: 'child-1',
parentPersonalDataUuid: 'parent-1',
date: '2026-01-06',
dayType: 'sjukpenning',
isDubbeldag: false,
createdAt: '2026-01-01',
updatedAt: '2026-01-01',
},
]
const { rerender } = render(
<CalendarWeek
{...defaultProps}
plannedDays={plannedDays1}
parentName="Alice"
/>,
)
expect(screen.getByText(/Alice · Semester/)).toBeInTheDocument()
expect(screen.queryByText(/Sjukpenning/)).not.toBeInTheDocument()
rerender(
<CalendarWeek
{...defaultProps}
plannedDays={plannedDays2}
parentName="Alice"
/>,
)
expect(screen.getByText(/Sjukpenning/)).toBeInTheDocument()
})
})
describe('§3.4/§3.6 calendarDaysByDate pass-through', () => {
it('passes matching calendar metadata down to the CalendarDay for that date', () => {
const calendarDaysByDate = new Map([
[
'2026-01-06',
{
date: '2026-01-06',
name: 'Trettondedag jul',
dayType: 'red_day' as const,
isOverride: false,
overrideIsWorkingDay: null,
isFree: true,
},
],
])
render(<CalendarWeek {...defaultProps} calendarDaysByDate={calendarDaysByDate} />)
const cell = screen.getByTestId('calendar-day-2026-01-06')
expect(cell).toHaveAttribute('data-day-type', 'red_day')
expect(cell).toHaveAttribute('data-holiday-name', 'Trettondedag jul')
expect(cell).toHaveAttribute('data-is-override', 'false')
})
it('passes override metadata down for an overridden date', () => {
const calendarDaysByDate = new Map([
[
'2026-01-07',
{
date: '2026-01-07',
name: null,
dayType: 'working_day' as const,
isOverride: true,
overrideIsWorkingDay: false,
isFree: true,
},
],
])
render(<CalendarWeek {...defaultProps} calendarDaysByDate={calendarDaysByDate} />)
const cell = screen.getByTestId('calendar-day-2026-01-07')
expect(cell).toHaveAttribute('data-is-override', 'true')
expect(cell).toHaveAttribute('data-override-is-working-day', 'false')
})
it('leaves days with no matching entry unaffected when calendarDaysByDate is omitted', () => {
render(<CalendarWeek {...defaultProps} />)
const cell = screen.getByTestId('calendar-day-2026-01-05')
expect(cell).toHaveAttribute('data-day-type', '')
expect(cell).toHaveAttribute('data-is-override', 'false')
})
})
})