Files
F-r-ldraledigplannerare/frontend/src/components/inputs/DayOverridesInput.test.tsx
T
jacobh2 c5481a05f0 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
2026-09-14 18:54:56 +02:00

283 lines
8.1 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { DayOverridesInput } from './DayOverridesInput'
describe('DayOverridesInput', () => {
const mockOverrides = [
{
id: 'do-1',
workspaceId: 'ws-1',
date: '2026-12-24',
isWorkingDay: true,
note: 'Jobbade på juldagen',
createdAt: '2026-01-01T00:00:00',
updatedAt: '2026-01-01T00:00:00',
},
{
id: 'do-2',
workspaceId: 'ws-1',
date: '2026-06-20',
isWorkingDay: false,
note: null,
createdAt: '2026-01-01T00:00:00',
updatedAt: '2026-01-01T00:00:00',
},
]
let mockOnAdd: ReturnType<typeof vi.fn>
let mockOnRemove: ReturnType<typeof vi.fn>
beforeEach(() => {
mockOnAdd = vi.fn().mockResolvedValue(undefined)
mockOnRemove = vi.fn().mockResolvedValue(undefined)
})
describe('rendering', () => {
it('renders list of existing overrides', () => {
render(
<DayOverridesInput
overrides={mockOverrides}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
expect(screen.getByText('24 december 2026')).toBeInTheDocument()
expect(screen.getByText('20 juni 2026')).toBeInTheDocument()
})
it('renders working day override with correct status', () => {
render(
<DayOverridesInput
overrides={mockOverrides}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
expect(screen.getByTestId('override-item-do-1').textContent).toContain('Jag jobbar den här dagen')
})
it('renders off day override with correct status', () => {
render(
<DayOverridesInput
overrides={mockOverrides}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
expect(screen.getByTestId('override-item-do-2').textContent).toContain('Jag är ledig den här dagen')
})
it('renders note when present', () => {
render(
<DayOverridesInput
overrides={mockOverrides}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
expect(screen.getByText('Anteckning: Jobbade på juldagen')).toBeInTheDocument()
})
it('renders empty state when no overrides', () => {
render(
<DayOverridesInput
overrides={[]}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
expect(screen.getByText('Du har inga undantag i kalendern ännu.')).toBeInTheDocument()
})
it('renders add form with all fields', () => {
render(
<DayOverridesInput
overrides={[]}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
expect(screen.getByTestId('override-date-input')).toBeInTheDocument()
expect(screen.getByTestId('override-working-radio')).toBeInTheDocument()
expect(screen.getByTestId('override-off-radio')).toBeInTheDocument()
expect(screen.getByTestId('override-note-input')).toBeInTheDocument()
expect(screen.getByTestId('add-override-button')).toBeInTheDocument()
})
})
describe('adding override', () => {
it('calls onAdd with correct payload when working day is selected', async () => {
render(
<DayOverridesInput
overrides={[]}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
fireEvent.change(screen.getByTestId('override-date-input'), { target: { value: '2026-12-24' } })
fireEvent.click(screen.getByTestId('override-working-radio'))
fireEvent.change(screen.getByTestId('override-note-input'), { target: { value: 'Jobbar denna dag' } })
fireEvent.click(screen.getByTestId('add-override-button'))
await waitFor(() => {
expect(mockOnAdd).toHaveBeenCalledWith({
workspaceId: 'ws-1',
date: '2026-12-24',
isWorkingDay: true,
note: 'Jobbar denna dag',
})
})
})
it('calls onAdd with correct payload when off day is selected', async () => {
render(
<DayOverridesInput
overrides={[]}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
fireEvent.change(screen.getByTestId('override-date-input'), { target: { value: '2026-06-20' } })
fireEvent.click(screen.getByTestId('override-off-radio'))
fireEvent.click(screen.getByTestId('add-override-button'))
await waitFor(() => {
expect(mockOnAdd).toHaveBeenCalledWith({
workspaceId: 'ws-1',
date: '2026-06-20',
isWorkingDay: false,
note: null,
})
})
})
it('clears form after successful add', async () => {
render(
<DayOverridesInput
overrides={[]}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
const dateInput = screen.getByTestId('override-date-input') as HTMLInputElement
const noteInput = screen.getByTestId('override-note-input') as HTMLInputElement
fireEvent.change(dateInput, { target: { value: '2026-12-24' } })
fireEvent.change(noteInput, { target: { value: 'Test note' } })
fireEvent.click(screen.getByTestId('add-override-button'))
await waitFor(() => {
expect(dateInput.value).toBe('')
expect(noteInput.value).toBe('')
})
})
it('disables submit button when date is empty', () => {
render(
<DayOverridesInput
overrides={[]}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
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', () => {
it('calls onRemove with correct id', async () => {
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')
})
})
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', () => {
it('disables form during loading', () => {
render(
<DayOverridesInput
overrides={[]}
isLoading={true}
onAdd={mockOnAdd}
onRemove={mockOnRemove}
workspaceId="ws-1"
/>
)
expect((screen.getByTestId('override-date-input') as HTMLInputElement).disabled).toBe(true)
expect((screen.getByTestId('add-override-button') as HTMLButtonElement).disabled).toBe(true)
})
})
})