76b1e712b9
Works through every numbered item in NextSteps260913.md section 3, plus the three known verification issues in section 4.1. Surface what the backend already computed: - 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies - 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible - 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt) Calendar cluster (one pass over the holiday data): - new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides, paginated four years at a time - 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal - 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact - 3.7 click month/year to advance - 3.8 per-year red-days list in Settings, statutory days plus overrides i18n: - 3.5 five backend-generated Swedish surfaces converted to message_key + data, following the tips_service pattern; old string fields replaced, not kept Flexible wishes (3.9): - PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function, deterministic tie-break, two-pass ordering, test matrix rows 15-24 - backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator - frontend: season-preset toggle in WishEditor, unplaced outcome rendering Fixes (4.1): - pin typecheck to the local tsc rather than npx resolving a newer TypeScript - ignore generated coverage/ and dist/ in the ESLint flat config - fix the flaky usePlanApply test properly, with waitFor rather than a retry Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines. Not yet verified in a browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
import { render, fireEvent, screen } from '@testing-library/react'
|
|
import { VacationDaysTable } from './VacationDaysTable'
|
|
|
|
const ROWS = [
|
|
{ year: 2026, totalDays: 10, savedDaysIn: 2 },
|
|
{ year: 2027, totalDays: 25, savedDaysIn: 0 },
|
|
{ year: 2028, totalDays: 25, savedDaysIn: 0 },
|
|
]
|
|
|
|
describe('VacationDaysTable', () => {
|
|
it('renders one row per year', () => {
|
|
render(<VacationDaysTable rows={ROWS} onSave={() => {}} />)
|
|
expect(screen.getByTestId('vacation-year-row-2026')).toBeInTheDocument()
|
|
expect(screen.getByTestId('vacation-year-row-2027')).toBeInTheDocument()
|
|
expect(screen.getByTestId('vacation-year-row-2028')).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows the total days and saved days for each year', () => {
|
|
render(<VacationDaysTable rows={ROWS} onSave={() => {}} />)
|
|
const totalInput = screen.getByTestId('vacation-total-days-2026') as HTMLInputElement
|
|
const savedInput = screen.getByTestId('vacation-saved-days-2026') as HTMLInputElement
|
|
expect(totalInput.value).toBe('10')
|
|
expect(savedInput.value).toBe('2')
|
|
})
|
|
|
|
it('calls onSave with the edited year and total days when Spara is clicked', () => {
|
|
const onSave = vi.fn()
|
|
render(<VacationDaysTable rows={ROWS} onSave={onSave} />)
|
|
|
|
const totalInput = screen.getByTestId('vacation-total-days-2027')
|
|
fireEvent.change(totalInput, { target: { value: '30' } })
|
|
fireEvent.click(screen.getByTestId('vacation-save-2027'))
|
|
|
|
expect(onSave).toHaveBeenCalledWith(2027, 30, 0)
|
|
})
|
|
|
|
it('calls onSave with edited saved-days-in value', () => {
|
|
const onSave = vi.fn()
|
|
render(<VacationDaysTable rows={ROWS} onSave={onSave} />)
|
|
|
|
const savedInput = screen.getByTestId('vacation-saved-days-2026')
|
|
fireEvent.change(savedInput, { target: { value: '5' } })
|
|
fireEvent.click(screen.getByTestId('vacation-save-2026'))
|
|
|
|
expect(onSave).toHaveBeenCalledWith(2026, 10, 5)
|
|
})
|
|
|
|
it('does not mix up edits between rows', () => {
|
|
const onSave = vi.fn()
|
|
render(<VacationDaysTable rows={ROWS} onSave={onSave} />)
|
|
|
|
fireEvent.change(screen.getByTestId('vacation-total-days-2026'), { target: { value: '15' } })
|
|
fireEvent.click(screen.getByTestId('vacation-save-2027'))
|
|
|
|
expect(onSave).toHaveBeenCalledWith(2027, 25, 0)
|
|
})
|
|
|
|
it('disables save buttons while saving', () => {
|
|
render(<VacationDaysTable rows={ROWS} onSave={() => {}} isSaving />)
|
|
expect(screen.getByTestId('vacation-save-2026')).toBeDisabled()
|
|
})
|
|
|
|
it('shows an empty message when there are no rows', () => {
|
|
render(<VacationDaysTable rows={[]} onSave={() => {}} />)
|
|
expect(screen.queryByTestId('vacation-days-table')).not.toBeInTheDocument()
|
|
expect(screen.getByText(/Inga år att visa/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows an assumed badge for a year with no entered data (§3.2)', () => {
|
|
const rows = [...ROWS, { year: 2029, totalDays: 25, savedDaysIn: 0, isAssumed: true }]
|
|
render(<VacationDaysTable rows={rows} onSave={() => {}} />)
|
|
|
|
expect(screen.getByTestId('vacation-assumed-badge-2029')).toBeInTheDocument()
|
|
expect(screen.queryByTestId('vacation-assumed-badge-2026')).not.toBeInTheDocument()
|
|
})
|
|
})
|