b76fb7417e
Warnings such as "vacation days at risk of being lost" kept showing after the planner had already resolved them. The planner itself was firing correctly -- PlanSyncProvider syncs on every plan-input change -- so the fault was in the warnings, in two independent places. The vacation-expiry warning was computed before allocation. It read `vacation_forfeiting`, which the loader derives from already-committed usage, while `_assign_tier3` deliberately spends at-risk vacation days first precisely to avoid that forfeiture. The figure therefore counted days the plan was in the middle of saving. `_vacation_expiry_warnings` now subtracts the vacation days this run actually placed for that parent and year, clamped at zero -- sound because the bank is FIFO, so a day spent in year Y retires one expiring day one-for-one. The full carry-forward simulation is still not duplicated inside the ledger. The Dashboard then never refreshed them. Its proposal came from a one-shot effect guarded by `proposal === null`, with no planVersion dependency, so the card kept rendering the pre-replan warning until remount -- while `syncPlan` was already returning a fresh proposal that both call sites discarded. PlanSyncProvider now exposes that proposal behind a monotonic counter, and usePlanProposal prefers it once any sync has completed, keeping `propose()` for the first load. Also adds a manual recompute button to Settings, which only bumps planVersion and reports what the resulting sync did, and fixes partner add/remove never bumping planVersion at all -- that gap meant gaining or losing a parent never re-ran the allocator, though it changes who days can be assigned to. Rename stays cosmetic. Verified in the browser: a 2031 warning of 15 days dropped to 12 after three vacation days were planned, without a reload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jPFUcCJ5AKXmPVC1anhXe
322 lines
8.9 KiB
TypeScript
322 lines
8.9 KiB
TypeScript
/**
|
|
* Shared test fixtures — return values of `src/context/*` and `src/hooks/*`.
|
|
*
|
|
* None of these hooks export their result interface (e.g. `useAnalytics`'s
|
|
* `UseAnalyticsResult` is private to useAnalytics.ts), and this file
|
|
* deliberately does not add exports to make that possible — `ReturnType<typeof
|
|
* theHook>` gets the same fully-typed result without touching source files.
|
|
* `import type` erases at compile time, so this doesn't pull the hooks'
|
|
* runtime code (or React) into the test bundle.
|
|
*/
|
|
|
|
import { vi } from 'vitest'
|
|
import type { useWorkspace } from '@/context/WorkspaceContext'
|
|
import type { useAuth } from '@/context/AuthContext'
|
|
import type { usePlanSyncContext } from '@/context/PlanSyncProvider'
|
|
import type { useAnalytics } from '@/hooks/useAnalytics'
|
|
import type { useParentalLeave } from '@/hooks/useParentalLeave'
|
|
import type { usePlannedDays } from '@/hooks/usePlannedDays'
|
|
import type { useScraperFetch } from '@/hooks/useScraperFetch'
|
|
import type { useWorkspaceMembers } from '@/hooks/useWorkspaceMembers'
|
|
import type { useOptimizationTips } from '@/hooks/useOptimizationTips'
|
|
import type { usePlannedDayExclusions } from '@/hooks/usePlannedDayExclusions'
|
|
import type { useDayOverrides } from '@/hooks/useDayOverrides'
|
|
import type { useLeaveWishes } from '@/hooks/useLeaveWishes'
|
|
import type { usePlanProposal } from '@/hooks/usePlanProposal'
|
|
import type { usePlanSync } from '@/hooks/usePlanSync'
|
|
import type { useVacationDays } from '@/hooks/useVacationDays'
|
|
import type { useChildSettings } from '@/hooks/useChildSettings'
|
|
import type { useCalendarDays } from '@/hooks/useCalendarDays'
|
|
import type { usePreschoolPlanningDays } from '@/hooks/usePreschoolPlanningDays'
|
|
import type { usePreschoolPlanningDaysList } from '@/hooks/usePreschoolPlanningDaysList'
|
|
import type { useShowWeekNumbers } from '@/hooks/useCalendarPreferences'
|
|
import { makeChild, makePersonalData, makeVacationDays, makeWorkspace, makeParentalLeaveFetch } from './mockData'
|
|
import { makeWorkspaceOverview } from './analytics'
|
|
import { makeCalendarWindow } from './calendar'
|
|
import { makePreschoolPlanningDays } from './workspace'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Contexts
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function makeWorkspaceContextValue(
|
|
overrides: Partial<ReturnType<typeof useWorkspace>> = {},
|
|
): ReturnType<typeof useWorkspace> {
|
|
const workspace = makeWorkspace()
|
|
const child = makeChild()
|
|
const parent = makePersonalData({ uuid: 'pd-a', name: 'Förälder A' })
|
|
return {
|
|
workspace,
|
|
isLoading: false,
|
|
error: null,
|
|
workspaceId: workspace.id,
|
|
setWorkspaceId: vi.fn(),
|
|
refetch: vi.fn(),
|
|
children: [{ id: child.id, name: child.name, birth_date: child.birthDate }],
|
|
selectedChildId: child.id,
|
|
selectChild: vi.fn(),
|
|
personalDataUuid: parent.uuid,
|
|
parentName: parent.name,
|
|
refreshContext: vi.fn(),
|
|
planVersion: 0,
|
|
bumpPlanVersion: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makePlanSyncContextValue(
|
|
overrides: Partial<ReturnType<typeof usePlanSyncContext>> = {},
|
|
): ReturnType<typeof usePlanSyncContext> {
|
|
return {
|
|
pendingEvictionPreview: null,
|
|
clearPendingEvictionPreview: vi.fn(),
|
|
latestProposal: null,
|
|
latestProposalVersion: 0,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makeAuthContextValue(
|
|
overrides: Partial<ReturnType<typeof useAuth>> = {},
|
|
): ReturnType<typeof useAuth> {
|
|
return {
|
|
accessToken: 'token-123',
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
login: vi.fn(),
|
|
register: vi.fn(),
|
|
logout: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Data-fetching hooks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function makeAnalyticsResult(
|
|
overrides: Partial<ReturnType<typeof useAnalytics>> = {},
|
|
): ReturnType<typeof useAnalytics> {
|
|
return {
|
|
data: makeWorkspaceOverview(),
|
|
isLoading: false,
|
|
error: null,
|
|
refresh: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makeParentalLeaveResult(
|
|
overrides: Partial<ReturnType<typeof useParentalLeave>> = {},
|
|
): ReturnType<typeof useParentalLeave> {
|
|
return {
|
|
data: makeParentalLeaveFetch(),
|
|
isLoading: false,
|
|
error: null,
|
|
refresh: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makePlannedDaysResult(
|
|
overrides: Partial<ReturnType<typeof usePlannedDays>> = {},
|
|
): ReturnType<typeof usePlannedDays> {
|
|
return {
|
|
data: [],
|
|
isLoading: false,
|
|
error: null,
|
|
refresh: vi.fn(),
|
|
add: vi.fn(),
|
|
update: vi.fn(),
|
|
remove: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makeScraperFetchResult(
|
|
overrides: Partial<ReturnType<typeof useScraperFetch>> = {},
|
|
): ReturnType<typeof useScraperFetch> {
|
|
return {
|
|
sessionId: null,
|
|
pollData: null,
|
|
resolvedIds: null,
|
|
error: null,
|
|
isUnauthorized: false,
|
|
start: vi.fn(),
|
|
cancel: vi.fn(),
|
|
isRunning: false,
|
|
isTerminal: false,
|
|
partnerDeduction: null,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makeWorkspaceMembersResult(
|
|
overrides: Partial<ReturnType<typeof useWorkspaceMembers>> = {},
|
|
): ReturnType<typeof useWorkspaceMembers> {
|
|
return {
|
|
data: [makePersonalData({ uuid: 'pd-a', name: 'Förälder A' })],
|
|
isLoading: false,
|
|
error: null,
|
|
refresh: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makeOptimizationTipsResult(
|
|
overrides: Partial<ReturnType<typeof useOptimizationTips>> = {},
|
|
): ReturnType<typeof useOptimizationTips> {
|
|
return {
|
|
data: [],
|
|
isLoading: false,
|
|
error: null,
|
|
refresh: vi.fn(),
|
|
dismiss: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makeDayOverridesResult(
|
|
overrides: Partial<ReturnType<typeof useDayOverrides>> = {},
|
|
): ReturnType<typeof useDayOverrides> {
|
|
return {
|
|
data: [],
|
|
isLoading: false,
|
|
isRefreshing: false,
|
|
error: null,
|
|
upsert: vi.fn(),
|
|
remove: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makePlannedDayExclusionsResult(
|
|
overrides: Partial<ReturnType<typeof usePlannedDayExclusions>> = {},
|
|
): ReturnType<typeof usePlannedDayExclusions> {
|
|
return {
|
|
data: [],
|
|
isLoading: false,
|
|
error: null,
|
|
exclude: vi.fn(),
|
|
restore: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makeLeaveWishesResult(
|
|
overrides: Partial<ReturnType<typeof useLeaveWishes>> = {},
|
|
): ReturnType<typeof useLeaveWishes> {
|
|
return {
|
|
data: [],
|
|
isLoading: false,
|
|
isRefreshing: false,
|
|
error: null,
|
|
refresh: vi.fn(),
|
|
add: vi.fn(),
|
|
update: vi.fn(),
|
|
remove: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makePlanProposalResult(
|
|
overrides: Partial<ReturnType<typeof usePlanProposal>> = {},
|
|
): ReturnType<typeof usePlanProposal> {
|
|
return {
|
|
data: null,
|
|
isLoading: false,
|
|
error: null,
|
|
propose: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makePlanSyncResult(
|
|
overrides: Partial<ReturnType<typeof usePlanSync>> = {},
|
|
): ReturnType<typeof usePlanSync> {
|
|
return {
|
|
isSyncing: false,
|
|
error: null,
|
|
lastChanges: null,
|
|
sync: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makeVacationDaysResult(
|
|
overrides: Partial<ReturnType<typeof useVacationDays>> = {},
|
|
): ReturnType<typeof useVacationDays> {
|
|
return {
|
|
data: [makeVacationDays()],
|
|
isLoading: false,
|
|
isRefreshing: false,
|
|
error: null,
|
|
upsert: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makeChildSettingsResult(
|
|
overrides: Partial<ReturnType<typeof useChildSettings>> = {},
|
|
): ReturnType<typeof useChildSettings> {
|
|
return {
|
|
data: makeChild(),
|
|
isLoading: false,
|
|
isRefreshing: false,
|
|
error: null,
|
|
update: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makeCalendarDaysResult(
|
|
overrides: Partial<ReturnType<typeof useCalendarDays>> = {},
|
|
): ReturnType<typeof useCalendarDays> {
|
|
return {
|
|
data: makeCalendarWindow(),
|
|
isLoading: false,
|
|
isRefreshing: false,
|
|
error: null,
|
|
refetch: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makePreschoolPlanningDaysResult(
|
|
overrides: Partial<ReturnType<typeof usePreschoolPlanningDays>> = {},
|
|
): ReturnType<typeof usePreschoolPlanningDays> {
|
|
return {
|
|
data: makePreschoolPlanningDays(),
|
|
isLoading: false,
|
|
isRefreshing: false,
|
|
error: null,
|
|
setPlanned: vi.fn(),
|
|
recordTaken: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
export function makePreschoolPlanningDaysListResult(
|
|
overrides: Partial<ReturnType<typeof usePreschoolPlanningDaysList>> = {},
|
|
): ReturnType<typeof usePreschoolPlanningDaysList> {
|
|
return {
|
|
data: [makePreschoolPlanningDays()],
|
|
isLoading: false,
|
|
isRefreshing: false,
|
|
error: null,
|
|
refresh: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Local-storage-backed preference hook (a tuple, not an object)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function makeShowWeekNumbersTuple(
|
|
show = false,
|
|
setShow: (show: boolean) => void = vi.fn(),
|
|
): ReturnType<typeof useShowWeekNumbers> {
|
|
return [show, setShow]
|
|
}
|