8e1cf3524c
The page opened with generic tiles and a bar reading forty-eight of ninety-six weeks, which was neither weeks nor connected to the age-four ceiling it appeared to describe. It is replaced by one headline and a bar per parent, in days, against each parent's own savable limit as the server reports it. The headline has two framings on the same arithmetic. Before anything is planned it says what there is to plan and what will be lost without a plan, because on the first day almost the whole entitlement is technically at risk and saying so bluntly reads as an accusation. Once a plan exists it warns about the days genuinely at stake. A timeline of the years to the fourth birthday answers what the next four years look like, which a month-at-a-time calendar cannot. Shortfalls, surplus advice and balances sit below the fold, read-only: applying is automatic now, so a second apply button would be a way to get two different answers. Rejection messages move to one shared place that resolves a parent's name, so an identifier can no longer surface in their place.
203 lines
8.1 KiB
TypeScript
203 lines
8.1 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { render, screen } from '@testing-library/react'
|
|
import { PlanFold } from './PlanFold'
|
|
import * as hooks from '@/hooks'
|
|
import {
|
|
makeLeaveWishesResult,
|
|
makePlanProposalResult,
|
|
makePersonalData,
|
|
makeLeaveWish,
|
|
makePlanProposal,
|
|
makeWindowOutcome,
|
|
makeSurplusAdvice,
|
|
makePoolBalance,
|
|
makeVacationBalance,
|
|
} from '../../../tests/fixtures'
|
|
|
|
vi.mock('@/hooks')
|
|
|
|
const members = [
|
|
makePersonalData({ uuid: 'pd-a', name: 'Alex' }),
|
|
makePersonalData({ uuid: 'pd-b', name: 'Sam' }),
|
|
]
|
|
|
|
describe('PlanFold', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
vi.mocked(hooks.useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [makeLeaveWish()] }))
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult())
|
|
})
|
|
|
|
it('is collapsed by default (a native <details> element)', () => {
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
const details = screen.getByTestId('plan-fold')
|
|
expect(details.tagName).toBe('DETAILS')
|
|
expect(details).not.toHaveAttribute('open')
|
|
})
|
|
|
|
it('shows a hint instead of a proposal when there are no wishes', () => {
|
|
vi.mocked(hooks.useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [] }))
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(screen.getByTestId('plan-fold-no-wishes')).toHaveTextContent(
|
|
'Lägg till önskemål under Planering för att se ett förslag här.',
|
|
)
|
|
})
|
|
|
|
it('shows the proposal error when the read-only propose call fails', () => {
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(
|
|
makePlanProposalResult({ error: 'Kunde inte beräkna förslag' }),
|
|
)
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(screen.getByTestId('plan-fold-proposal-error')).toHaveTextContent('Kunde inte beräkna förslag')
|
|
})
|
|
|
|
it('auto-proposes once wishes are loaded, without a manual button press', () => {
|
|
const propose = vi.fn()
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult({ propose }))
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(propose).toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not propose again once a proposal is already loaded', () => {
|
|
const propose = vi.fn()
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(
|
|
makePlanProposalResult({ propose, data: makePlanProposal() }),
|
|
)
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(propose).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not propose while there are no wishes yet', () => {
|
|
const propose = vi.fn()
|
|
vi.mocked(hooks.useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [] }))
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult({ propose }))
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(propose).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not propose while wishes are still loading', () => {
|
|
const propose = vi.fn()
|
|
vi.mocked(hooks.useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ isLoading: true, data: [] }))
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult({ propose }))
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(propose).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not propose again while a propose call is already in flight', () => {
|
|
const propose = vi.fn()
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult({ propose, isLoading: true }))
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(propose).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('renders per-period wished-vs-funded from the proposal', () => {
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(
|
|
makePlanProposalResult({
|
|
data: makePlanProposal({
|
|
outcomes: [makeWindowOutcome({ wishId: 'lw-1', wishName: 'Sommar', year: 2026, weeksWished: 6, weeksFunded: 4 })],
|
|
}),
|
|
}),
|
|
)
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(screen.getByTestId('plan-fold-overview-year-2026')).toHaveTextContent('Sommar: 4/6 veckor')
|
|
})
|
|
|
|
it('shows the empty-horizon message when the proposal has no outcomes', () => {
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(
|
|
makePlanProposalResult({ data: makePlanProposal({ outcomes: [] }) }),
|
|
)
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(screen.getByTestId('plan-fold-overview')).toHaveTextContent('Inga fönster inom planeringshorisonten.')
|
|
})
|
|
|
|
it('renders surplus advice with a resolved parent name, never a raw uuid', () => {
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(
|
|
makePlanProposalResult({
|
|
data: makePlanProposal({
|
|
surplusAdvice: [
|
|
makeSurplusAdvice({
|
|
kind: 'wish_more_leave',
|
|
messageKey: 'planSurplusAdvice.wish_more_leave',
|
|
daysInvolved: 12,
|
|
data: { parent_personal_data_uuid: 'pd-b', child_id: 'child-1', weeks: 2 },
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
)
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
const advice = screen.getByTestId('plan-fold-surplus-advice-0')
|
|
expect(advice).toHaveTextContent('Sam')
|
|
expect(advice).not.toHaveTextContent('pd-b')
|
|
})
|
|
|
|
it('falls back to a generic label for surplus advice when no member matches', () => {
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(
|
|
makePlanProposalResult({
|
|
data: makePlanProposal({
|
|
surplusAdvice: [
|
|
makeSurplusAdvice({
|
|
kind: 'wish_more_leave',
|
|
messageKey: 'planSurplusAdvice.wish_more_leave',
|
|
daysInvolved: 12,
|
|
data: { parent_personal_data_uuid: 'pd-unknown', child_id: 'child-1', weeks: 2 },
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
)
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
const advice = screen.getByTestId('plan-fold-surplus-advice-0')
|
|
expect(advice).not.toHaveTextContent('pd-unknown')
|
|
})
|
|
|
|
it('renders an unrecognised surplus advice kind/messageKey by falling back to the raw value', () => {
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(
|
|
makePlanProposalResult({
|
|
data: makePlanProposal({
|
|
surplusAdvice: [
|
|
makeSurplusAdvice({
|
|
kind: 'brand_new_kind',
|
|
messageKey: 'planSurplusAdvice.brand_new_kind',
|
|
data: {},
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
)
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
const advice = screen.getByTestId('plan-fold-surplus-advice-0')
|
|
expect(advice).toHaveTextContent('brand_new_kind')
|
|
})
|
|
|
|
it('does not render a surplus-advice section when there is none', () => {
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(
|
|
makePlanProposalResult({ data: makePlanProposal({ surplusAdvice: [] }) }),
|
|
)
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(screen.queryByTestId('plan-fold-surplus-advice')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('renders pool and vacation balance tables', () => {
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(
|
|
makePlanProposalResult({
|
|
data: makePlanProposal({
|
|
poolBalances: [makePoolBalance({ parentPersonalDataUuid: 'pd-a', sjukpenningLeft: 300, lagstaLeft: 80, dubbeldagarLeft: 50 })],
|
|
vacationBalances: [makeVacationBalance({ parentPersonalDataUuid: 'pd-a', year: 2026, alreadyUsed: 10, totalDays: 25, savedDaysIn: 0 })],
|
|
}),
|
|
}),
|
|
)
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(screen.getByTestId('plan-fold-pool-balances')).toHaveTextContent('Alex')
|
|
expect(screen.getByTestId('plan-fold-pool-balances')).toHaveTextContent('300')
|
|
expect(screen.getByTestId('plan-fold-vacation-balances')).toHaveTextContent('25')
|
|
})
|
|
|
|
it('renders nothing beyond the summary/hint when there is no proposal yet', () => {
|
|
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult({ data: null }))
|
|
render(<PlanFold workspaceId="ws-1" members={members} />)
|
|
expect(screen.queryByTestId('plan-fold-overview')).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('plan-fold-balances')).not.toBeInTheDocument()
|
|
})
|
|
})
|