Redesign the setup flow and consolidate the CSS into one primitives layer

Onboarding asks explicitly for one or two parents and shows one card per
parent for balances; the balance form is per parent with inline
validation; the partner can be filled from the FK deduction or added
from it. Settings and Dashboard reuse the same form.

CSS: bare buttons carry no colour any more; buttons, cards, choice
cards, fields, alerts, badges, eyebrow and modal chrome are defined once
in primitives.css. Media queries live only in responsive.css. A style
guard test blocks hex, !important and stray media queries. Bilingual
fixes: calendar selects size by content, header no longer wraps. Vitest
is capped at four workers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 12:27:29 +02:00
parent e4719cb9ae
commit ae6db7e16f
105 changed files with 5683 additions and 3690 deletions
+142 -1
View File
@@ -2,13 +2,21 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { usePlannedDays } from './usePlannedDays'
import * as api from '@/services/api'
import { makePlannedLeaveDay } from '../../tests/fixtures'
import { useWorkspace } from '@/context'
import { makePlannedLeaveDay, makeWorkspaceContextValue } from '../../tests/fixtures'
vi.mock('@/services/api')
vi.mock('@/context')
describe('usePlannedDays', () => {
let bumpPlanVersion: ReturnType<typeof vi.fn>
beforeEach(() => {
vi.clearAllMocks()
bumpPlanVersion = vi.fn()
vi.mocked(useWorkspace).mockReturnValue(
makeWorkspaceContextValue({ planVersion: 0, bumpPlanVersion }),
)
})
it('initializes with null data', () => {
@@ -419,4 +427,137 @@ describe('usePlannedDays', () => {
expect(removeError?.message).toBe('Delete failed')
})
describe('plan-version invalidation', () => {
it('bumps planVersion after a successful add', async () => {
vi.mocked(api.getPlannedDays).mockResolvedValue([])
vi.mocked(api.createPlannedDay).mockResolvedValue(
makePlannedLeaveDay({ id: '1', date: '2026-06-16' }),
)
const { result } = renderHook(() => usePlannedDays('child1'))
await act(async () => {
await result.current.add({
workspace_id: 'ws1',
child_id: 'child1',
date: '2026-06-16',
day_type: 'vacation',
parent_personal_data_uuid: 'parent1',
})
})
expect(bumpPlanVersion).toHaveBeenCalledTimes(1)
})
it('bumps planVersion after a successful update', async () => {
vi.mocked(api.getPlannedDays).mockResolvedValue([])
vi.mocked(api.updatePlannedDay).mockResolvedValue(makePlannedLeaveDay({ id: '1' }))
const { result } = renderHook(() => usePlannedDays('child1'))
await act(async () => {
await result.current.update('1', { day_type: 'lagsta' })
})
expect(bumpPlanVersion).toHaveBeenCalledTimes(1)
})
it('bumps planVersion after a successful remove', async () => {
vi.mocked(api.getPlannedDays).mockResolvedValue([])
vi.mocked(api.deletePlannedDay).mockResolvedValue(undefined)
const { result } = renderHook(() => usePlannedDays('child1'))
await act(async () => {
await result.current.remove('1')
})
expect(bumpPlanVersion).toHaveBeenCalledTimes(1)
})
it('does not bump planVersion when a mutation fails', async () => {
vi.mocked(api.getPlannedDays).mockResolvedValue([])
vi.mocked(api.createPlannedDay).mockRejectedValue(new Error('boom'))
const { result } = renderHook(() => usePlannedDays('child1'))
await act(async () => {
try {
await result.current.add({
workspace_id: 'ws1',
child_id: 'child1',
date: '2026-06-16',
day_type: 'vacation',
parent_personal_data_uuid: 'parent1',
})
} catch {
// expected
}
})
expect(bumpPlanVersion).not.toHaveBeenCalled()
})
it('refetches when planVersion changes, even without a local mutation (invalidation from another instance)', async () => {
vi.mocked(api.getPlannedDays).mockResolvedValue([])
const { rerender } = renderHook(() => usePlannedDays('child1'))
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
})
const callCountBefore = vi.mocked(api.getPlannedDays).mock.calls.length
// Simulate another usePlannedDays instance bumping the shared counter.
vi.mocked(useWorkspace).mockReturnValue(
makeWorkspaceContextValue({ planVersion: 1, bumpPlanVersion }),
)
rerender()
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
})
expect(vi.mocked(api.getPlannedDays).mock.calls.length).toBeGreaterThan(callCountBefore)
})
it('does not loop: bumping its own planVersion after a mutation causes one extra refetch, not repeated ones', async () => {
vi.mocked(api.getPlannedDays).mockResolvedValue([])
vi.mocked(api.createPlannedDay).mockResolvedValue(
makePlannedLeaveDay({ id: '1', date: '2026-06-16' }),
)
// bumpPlanVersion actually bumps a real counter here (rather than a
// no-op mock) so the effect's dependency genuinely changes once, the
// same way the real WorkspaceProvider would.
let planVersion = 0
vi.mocked(useWorkspace).mockImplementation(() =>
makeWorkspaceContextValue({
planVersion,
bumpPlanVersion: () => {
planVersion += 1
},
}),
)
const { result, rerender } = renderHook(() => usePlannedDays('child1'))
await act(async () => {
await result.current.add({
workspace_id: 'ws1',
child_id: 'child1',
date: '2026-06-16',
day_type: 'vacation',
parent_personal_data_uuid: 'parent1',
})
})
rerender()
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
})
// Own direct fetch() after add, plus one effect-triggered refetch from
// its own bump — not an unbounded/growing number of calls.
const callCountAfterFirstSettle = vi.mocked(api.getPlannedDays).mock.calls.length
rerender()
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
})
expect(vi.mocked(api.getPlannedDays).mock.calls.length).toBe(callCountAfterFirstSettle)
})
})
})