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:
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user