c5481a05f0
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
283 lines
8.1 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|