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:
@@ -1,11 +1,11 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import { useSearchParams, type SetURLSearchParams } from 'react-router-dom'
|
||||
import { Calendar } from './Calendar'
|
||||
import { useWorkspace } from '@/context'
|
||||
import {
|
||||
usePlannedDays,
|
||||
useCalendarDays,
|
||||
useDayOverrides,
|
||||
useShowWeekNumbers,
|
||||
getLastLeaveType,
|
||||
useLeaveWishes,
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
makeWorkspaceContextValue,
|
||||
makePlannedDaysResult,
|
||||
makeCalendarDaysResult,
|
||||
makeDayOverridesResult,
|
||||
makeShowWeekNumbersTuple,
|
||||
makePlannedLeaveDay,
|
||||
makeLeaveWishesResult,
|
||||
@@ -41,13 +40,22 @@ vi.mock('@/context', () => ({
|
||||
useWorkspace: vi.fn(),
|
||||
}))
|
||||
|
||||
// Calendar.tsx only reads `useSearchParams` from react-router-dom (no
|
||||
// `useNavigate`/route matching), so the rest of the module can stay real —
|
||||
// only the search params themselves need to be controllable per test.
|
||||
vi.mock('react-router-dom', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('react-router-dom')>()
|
||||
return { ...actual, useSearchParams: vi.fn() }
|
||||
})
|
||||
|
||||
const mockSetSearchParams: SetURLSearchParams = vi.fn()
|
||||
|
||||
vi.mock('@/hooks', () => ({
|
||||
usePlannedDays: vi.fn(),
|
||||
useShowWeekNumbers: vi.fn(() => [false]),
|
||||
getLastLeaveType: vi.fn(() => 'vacation'),
|
||||
setLastLeaveType: vi.fn(),
|
||||
useCalendarDays: vi.fn(),
|
||||
useDayOverrides: vi.fn(),
|
||||
useLeaveWishes: vi.fn(),
|
||||
useWorkspaceMembers: vi.fn(),
|
||||
usePlanSync: vi.fn(),
|
||||
@@ -64,15 +72,21 @@ vi.mock('@/components/common/LoadingSpinner', () => ({
|
||||
vi.mock('@/components/modals/PlannedDayModal', () => ({
|
||||
// Exposes the props this test suite cares about as data attributes/buttons
|
||||
// so tests can assert on Calendar.tsx's wiring without rendering the real
|
||||
// modal (that component has its own exhaustive test suite).
|
||||
PlannedDayModal: (props: PlannedDayModalProps) => (
|
||||
// modal (that component has its own exhaustive test suite). Mirrors the
|
||||
// real component's `if (!isOpen) return null` — Calendar.tsx can have a
|
||||
// non-empty `selectedDates` (so `firstSelectedDate` is set, and this mock
|
||||
// gets rendered) while `isModalOpen` is still false, e.g. right after
|
||||
// seeding the selection from a `?date=` param without opening the modal.
|
||||
PlannedDayModal: (props: PlannedDayModalProps) => {
|
||||
if (!props.isOpen) return null
|
||||
return (
|
||||
<div
|
||||
data-testid="modal"
|
||||
data-default-day-type={props.defaultDayType ?? ''}
|
||||
data-existing-day={props.existingDay ? 'yes' : 'no'}
|
||||
data-parents-count={props.parents.length}
|
||||
data-day-info={props.dayInfo ? 'yes' : 'no'}
|
||||
data-override-error={props.overrideError ?? ''}
|
||||
data-is-range-mode={props.isRangeMode ? 'yes' : 'no'}
|
||||
data-day-count={props.dayCount ?? ''}
|
||||
>
|
||||
<button
|
||||
data-testid="modal-save"
|
||||
@@ -85,16 +99,12 @@ vi.mock('@/components/modals/PlannedDayModal', () => ({
|
||||
delete
|
||||
</button>
|
||||
)}
|
||||
{props.onToggleOverride && (
|
||||
<button data-testid="modal-toggle-override" onClick={() => void props.onToggleOverride?.()}>
|
||||
toggle
|
||||
</button>
|
||||
)}
|
||||
<button data-testid="modal-close" onClick={props.onClose}>
|
||||
close
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/components/calendar/PeriodPopover', () => ({
|
||||
@@ -164,19 +174,18 @@ describe('Calendar', () => {
|
||||
|
||||
const mockPlannedDays = makePlannedDaysResult()
|
||||
const mockCalendarWindow = makeCalendarDaysResult()
|
||||
const mockDayOverrides = makeDayOverridesResult()
|
||||
const mockLeaveWishes = makeLeaveWishesResult()
|
||||
const mockMembers = makeWorkspaceMembersResult()
|
||||
const mockSync = makePlanSyncResult()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useCalendarDays).mockReturnValue(mockCalendarWindow)
|
||||
vi.mocked(useDayOverrides).mockReturnValue(mockDayOverrides)
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(mockLeaveWishes)
|
||||
vi.mocked(useWorkspaceMembers).mockReturnValue(mockMembers)
|
||||
vi.mocked(usePlanSync).mockReturnValue(mockSync)
|
||||
vi.mocked(useWorkspace).mockReturnValue(mockWorkspace)
|
||||
vi.mocked(usePlannedDays).mockReturnValue(mockPlannedDays)
|
||||
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams(), mockSetSearchParams])
|
||||
})
|
||||
|
||||
function todayDateString(): string {
|
||||
@@ -212,6 +221,22 @@ describe('Calendar', () => {
|
||||
expect(calendarDays.length % 7).toBe(0)
|
||||
})
|
||||
|
||||
it('does not render a phantom trailing week when the month ends exactly on a Sunday', () => {
|
||||
// May 2026 has 31 days, starting on a Friday: 4 leading days + 31 = 35,
|
||||
// an exact multiple of 7 — the naive "always pad and push a trailing
|
||||
// week" logic would add a 6th row of 7 disabled June cells that serves
|
||||
// no purpose.
|
||||
render(<Calendar />)
|
||||
const title = screen.getByRole('heading', { level: 2 })
|
||||
const yearSelect = title.querySelector('.calendar__year-select') as HTMLSelectElement
|
||||
const monthSelect = title.querySelector('.calendar__month-select') as HTMLSelectElement
|
||||
fireEvent.change(yearSelect, { target: { value: '2026' } })
|
||||
fireEvent.change(monthSelect, { target: { value: '4' } })
|
||||
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
expect(calendarDays.length).toBe(35)
|
||||
})
|
||||
|
||||
it('opens the day popover when clicking a current-month day', () => {
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
@@ -223,7 +248,12 @@ describe('Calendar', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not open the day popover when clicking disabled (non-current-month) day', () => {
|
||||
it('opens the day popover when clicking a disabled (adjacent-month) day — planning a visible day outside the displayed month is deliberate, not dead', () => {
|
||||
// Decided behaviour for the drag-across-month-boundary fix: a leading/
|
||||
// trailing cell stays dimmed (`calendar-day--disabled` — see
|
||||
// CalendarDay.tsx) but is still clickable/drag-selectable, same as any
|
||||
// other visible day. Silently doing nothing on a visible cell was the
|
||||
// original bug (see CalendarWeek.tsx's day-cell handlers).
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const disabledDay = calendarDays.find((el) => el.classList.contains('calendar-day--disabled'))
|
||||
@@ -231,6 +261,7 @@ describe('Calendar', () => {
|
||||
if (disabledDay) {
|
||||
fireEvent.click(disabledDay)
|
||||
expect(disabledDay).toHaveClass('calendar-day--disabled')
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -297,6 +328,59 @@ describe('Calendar', () => {
|
||||
expect(options).toContain(new Date().getFullYear())
|
||||
})
|
||||
|
||||
describe('?date= query param seeds the initial month/selection', () => {
|
||||
it('opens the month of a valid future ?date= and highlights that day, without opening the modal', () => {
|
||||
vi.mocked(useSearchParams).mockReturnValue([
|
||||
new URLSearchParams('date=2030-07-15'),
|
||||
mockSetSearchParams,
|
||||
])
|
||||
|
||||
render(<Calendar />)
|
||||
|
||||
const title = screen.getByRole('heading', { level: 2 })
|
||||
const yearSelect = title.querySelector('.calendar__year-select') as HTMLSelectElement
|
||||
const monthSelect = title.querySelector('.calendar__month-select') as HTMLSelectElement
|
||||
expect(yearSelect.value).toBe('2030')
|
||||
expect(monthSelect.value).toBe('6') // July is month index 6
|
||||
|
||||
const targetDay = screen
|
||||
.getAllByTestId('calendar-day')
|
||||
.find((el) => el.getAttribute('aria-label')?.startsWith('2030-07-15'))
|
||||
expect(targetDay).toHaveClass('calendar-day--selected')
|
||||
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('falls back to today for an invalid/garbage ?date= param', () => {
|
||||
vi.mocked(useSearchParams).mockReturnValue([
|
||||
new URLSearchParams('date=not-a-real-date'),
|
||||
mockSetSearchParams,
|
||||
])
|
||||
|
||||
render(<Calendar />)
|
||||
|
||||
const today = new Date()
|
||||
const title = screen.getByRole('heading', { level: 2 })
|
||||
const yearSelect = title.querySelector('.calendar__year-select') as HTMLSelectElement
|
||||
const monthSelect = title.querySelector('.calendar__month-select') as HTMLSelectElement
|
||||
expect(yearSelect.value).toBe(String(today.getFullYear()))
|
||||
expect(monthSelect.value).toBe(String(today.getMonth()))
|
||||
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('falls back to today when no ?date= param is present', () => {
|
||||
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams(), mockSetSearchParams])
|
||||
|
||||
render(<Calendar />)
|
||||
|
||||
const today = new Date()
|
||||
const title = screen.getByRole('heading', { level: 2 })
|
||||
const yearSelect = title.querySelector('.calendar__year-select') as HTMLSelectElement
|
||||
const monthSelect = title.querySelector('.calendar__month-select') as HTMLSelectElement
|
||||
expect(yearSelect.value).toBe(String(today.getFullYear()))
|
||||
expect(monthSelect.value).toBe(String(today.getMonth()))
|
||||
})
|
||||
})
|
||||
|
||||
describe('§3.4 red day / klämdag shading', () => {
|
||||
it('shades a statutory red day and shows its name', () => {
|
||||
const today = todayDateString()
|
||||
@@ -320,90 +404,6 @@ describe('Calendar', () => {
|
||||
expect(screen.queryByTestId('calendar-override-toggle')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('calendar-override-hint')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('passes the clicked date\'s calendar metadata as dayInfo to the day popover', () => {
|
||||
const today = todayDateString()
|
||||
vi.mocked(useCalendarDays).mockReturnValue(
|
||||
makeCalendarDaysResult({
|
||||
data: makeCalendarWindow({ days: [makeCalendarDayEntry({ date: today, isFree: true })] }),
|
||||
}),
|
||||
)
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const todayCell = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith(today))
|
||||
fireEvent.click(todayCell!)
|
||||
expect(screen.getByTestId('modal')).toHaveAttribute('data-day-info', 'yes')
|
||||
})
|
||||
|
||||
it('toggling the day popover\'s override switch marks a free day as a working day', async () => {
|
||||
const today = todayDateString()
|
||||
const upsert = vi.fn().mockResolvedValue(undefined)
|
||||
const refetchCalendarWindow = vi.fn().mockResolvedValue(undefined)
|
||||
vi.mocked(useCalendarDays).mockReturnValue(
|
||||
makeCalendarDaysResult({
|
||||
data: makeCalendarWindow({ days: [makeCalendarDayEntry({ date: today, dayType: 'weekend', isFree: true })] }),
|
||||
refetch: refetchCalendarWindow,
|
||||
}),
|
||||
)
|
||||
vi.mocked(useDayOverrides).mockReturnValue(makeDayOverridesResult({ data: [], upsert, remove: vi.fn() }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const todayCell = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith(today))
|
||||
fireEvent.click(todayCell!)
|
||||
fireEvent.click(screen.getByTestId('modal-toggle-override'))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(upsert).toHaveBeenCalledWith({ workspaceId: 'ws-1', date: today, isWorkingDay: true, note: null }),
|
||||
)
|
||||
await waitFor(() => expect(refetchCalendarWindow).toHaveBeenCalled())
|
||||
})
|
||||
|
||||
it('removes an existing override when toggled again', async () => {
|
||||
const today = todayDateString()
|
||||
const removeOverride = vi.fn().mockResolvedValue(undefined)
|
||||
vi.mocked(useCalendarDays).mockReturnValue(
|
||||
makeCalendarDaysResult({
|
||||
data: makeCalendarWindow({
|
||||
days: [makeCalendarDayEntry({ date: today, isOverride: true, overrideIsWorkingDay: false, isFree: true })],
|
||||
}),
|
||||
}),
|
||||
)
|
||||
vi.mocked(useDayOverrides).mockReturnValue(
|
||||
makeDayOverridesResult({
|
||||
data: [{ id: 'ov-1', workspaceId: 'ws-1', date: today, isWorkingDay: false, note: null, createdAt: null, updatedAt: null }],
|
||||
upsert: vi.fn(),
|
||||
remove: removeOverride,
|
||||
}),
|
||||
)
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const todayCell = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith(today))
|
||||
fireEvent.click(todayCell!)
|
||||
fireEvent.click(screen.getByTestId('modal-toggle-override'))
|
||||
|
||||
await waitFor(() => expect(removeOverride).toHaveBeenCalledWith('ov-1'))
|
||||
})
|
||||
|
||||
it('surfaces an override failure through the modal\'s overrideError prop', async () => {
|
||||
const today = todayDateString()
|
||||
const upsert = vi.fn().mockRejectedValue(new Error('quota exceeded'))
|
||||
vi.mocked(useCalendarDays).mockReturnValue(
|
||||
makeCalendarDaysResult({
|
||||
data: makeCalendarWindow({ days: [makeCalendarDayEntry({ date: today, dayType: 'weekend', isFree: true })] }),
|
||||
}),
|
||||
)
|
||||
vi.mocked(useDayOverrides).mockReturnValue(makeDayOverridesResult({ data: [], upsert, remove: vi.fn() }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const todayCell = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith(today))
|
||||
fireEvent.click(todayCell!)
|
||||
fireEvent.click(screen.getByTestId('modal-toggle-override'))
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('modal')).toHaveAttribute('data-override-error', 'quota exceeded'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('§Task7 saving a day pins it', () => {
|
||||
@@ -500,15 +500,31 @@ describe('Calendar', () => {
|
||||
fireEvent.click(currentMonthDay!)
|
||||
expect(screen.getByTestId('modal')).toHaveAttribute('data-parents-count', '1')
|
||||
})
|
||||
|
||||
it('labels a partner-owned band with their actual name, not the generic "Partner" string', () => {
|
||||
const today = todayDateString()
|
||||
vi.mocked(useWorkspaceMembers).mockReturnValue(
|
||||
makeWorkspaceMembersResult({
|
||||
data: [makePersonalData({ uuid: 'person-1', name: 'Förälder A' }), makePersonalData({ uuid: 'pd-b', name: 'Sam' })],
|
||||
}),
|
||||
)
|
||||
vi.mocked(usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
data: [makePlannedLeaveDay({ id: 'pd-1', date: today, wishId: null, parentPersonalDataUuid: 'pd-b' })],
|
||||
}),
|
||||
)
|
||||
|
||||
render(<Calendar />)
|
||||
|
||||
expect(screen.getByText(/Sam ·/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/^Partner ·/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('§Task7 drag-select creates a period', () => {
|
||||
it('creates a fixed, once wish covering the dragged range and syncs', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-new' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
const sync = vi.fn().mockResolvedValue(makePlanSyncResponse())
|
||||
describe('§Task7 drag-select opens the day popover on the exact dragged days', () => {
|
||||
it('opens the day popover pre-selected on every dragged date, without creating a wish', async () => {
|
||||
const add = vi.fn()
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [] }))
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
@@ -520,21 +536,16 @@ describe('Calendar', () => {
|
||||
fireEvent.pointerEnter(endDay)
|
||||
fireEvent.pointerUp(window)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(add).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
recurrence: 'once',
|
||||
windowMode: 'fixed',
|
||||
parentMode: 'specific',
|
||||
parentPersonalDataUuid: 'person-1',
|
||||
}),
|
||||
),
|
||||
)
|
||||
// No day popover opens for a multi-day drag — the period is created directly.
|
||||
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
|
||||
const modal = await screen.findByTestId('modal')
|
||||
expect(modal).toHaveAttribute('data-is-range-mode', 'yes')
|
||||
expect(modal).toHaveAttribute('data-day-count', '4')
|
||||
// A drag (single- or multi-day) never creates a LeaveWish/synced
|
||||
// period on its own anymore — only the popover's own save does
|
||||
// anything, and that writes planned days directly.
|
||||
expect(add).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not open the day popover for a single-day drag (falls back to the normal click path)', () => {
|
||||
it('opens the day popover in single mode for a single-day drag (falls back to the normal click path)', () => {
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDay = calendarDays.find((el) => !el.classList.contains('calendar-day--disabled'))!
|
||||
@@ -542,21 +553,33 @@ describe('Calendar', () => {
|
||||
fireEvent.pointerDown(currentMonthDay)
|
||||
fireEvent.pointerUp(window)
|
||||
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
const modal = screen.getByTestId('modal')
|
||||
expect(modal).toBeInTheDocument()
|
||||
expect(modal).toHaveAttribute('data-is-range-mode', 'no')
|
||||
})
|
||||
|
||||
it('shows an error toast when creating the period fails', async () => {
|
||||
const add = vi.fn().mockRejectedValue(new Error('validation failed'))
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [] }))
|
||||
|
||||
it('a leading week\'s in-month days are draggable (regression: the week used to be gated by weekDates[0]\'s month, not the displayed month)', () => {
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
const title = screen.getByRole('heading', { level: 2 })
|
||||
const yearSelect = title.querySelector('.calendar__year-select') as HTMLSelectElement
|
||||
const monthSelect = title.querySelector('.calendar__month-select') as HTMLSelectElement
|
||||
// March 2026 starts on a Sunday, so its first calendar row is a
|
||||
// leading week made up of six February days plus March 1st.
|
||||
fireEvent.change(yearSelect, { target: { value: '2026' } })
|
||||
fireEvent.change(monthSelect, { target: { value: '2' } })
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('calendar-toast')).toHaveTextContent('validation failed'))
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const marchFirst = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith('2026-03-01'))!
|
||||
const febTail = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith('2026-02-23'))!
|
||||
|
||||
// The previous month's tail day in that same row stays inert...
|
||||
expect(febTail).toHaveClass('calendar-day--disabled')
|
||||
// ...but March 1st — in-month, in the very same leading week — is not.
|
||||
expect(marchFirst).not.toHaveClass('calendar-day--disabled')
|
||||
|
||||
fireEvent.pointerDown(marchFirst)
|
||||
fireEvent.pointerUp(window)
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -773,46 +796,42 @@ describe('Calendar', () => {
|
||||
})
|
||||
|
||||
describe('§Task7 sync toast + undo', () => {
|
||||
// Only the preset rail and period-popover edits still create/sync a
|
||||
// `LeaveWish` — a drag no longer does (it just plans exact days
|
||||
// directly, with no sync/toast). These tests drive the toast/undo
|
||||
// machinery through the preset rail instead.
|
||||
it('shows a toast summarizing what changed after a sync with changes', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-new', name: 'Sommar' })
|
||||
const created = makeLeaveWish({ id: 'lw-preset', name: 'Sommar' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
const sync = vi.fn().mockResolvedValue(
|
||||
makePlanSyncResponse({ changes: [makePlanChange({ newWishId: 'lw-new' }), makePlanChange({ newWishId: 'lw-new' })] }),
|
||||
makePlanSyncResponse({ changes: [makePlanChange({ newWishId: 'lw-preset' }), makePlanChange({ newWishId: 'lw-preset' })] }),
|
||||
)
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [created] }))
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
fireEvent.click(screen.getByTestId('preset-click'))
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('calendar-toast')).toHaveTextContent('Sommar'))
|
||||
expect(screen.getByTestId('calendar-toast')).toHaveTextContent('2')
|
||||
})
|
||||
|
||||
it('shows no toast when the sync produces no changes', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-new' })
|
||||
const created = makeLeaveWish({ id: 'lw-preset' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
const sync = vi.fn().mockResolvedValue(makePlanSyncResponse({ changes: [] }))
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [] }))
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
fireEvent.click(screen.getByTestId('preset-click'))
|
||||
|
||||
await waitFor(() => expect(add).toHaveBeenCalled())
|
||||
expect(screen.queryByTestId('calendar-toast')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('explains a fully-starved sync (no changes, nothing funded) instead of showing nothing', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-new' })
|
||||
const created = makeLeaveWish({ id: 'lw-preset' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
const sync = vi.fn().mockResolvedValue(
|
||||
makePlanSyncResponse({
|
||||
@@ -820,7 +839,7 @@ describe('Calendar', () => {
|
||||
proposal: makePlanProposal({
|
||||
outcomes: [
|
||||
makeWindowOutcome({
|
||||
wishId: 'lw-new',
|
||||
wishId: 'lw-preset',
|
||||
startDate: '2026-06-01',
|
||||
endDate: '2026-06-14',
|
||||
weeksWished: 2,
|
||||
@@ -836,34 +855,34 @@ describe('Calendar', () => {
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
fireEvent.click(screen.getByTestId('preset-click'))
|
||||
|
||||
const toast = await screen.findByTestId('calendar-toast')
|
||||
expect(toast).toHaveTextContent('Sommar')
|
||||
expect(toast).toHaveTextContent('10')
|
||||
})
|
||||
|
||||
it('reports a calendar-drawn period\'s shortfall in days, not weeks, since it carries an exact targetDays', async () => {
|
||||
// Regression: a drag-created wish sets `targetDays` (an exact day
|
||||
// count); `targetWeeks` on that same wish is just a rough
|
||||
it('reports a period\'s shortfall in days, not weeks, when the wish carries an exact targetDays', async () => {
|
||||
// `targetDays` (an exact day count) can still be set on a wish (e.g.
|
||||
// one created before this drag-select rework, or seeded directly);
|
||||
// `targetWeeks` on that same wish is just a rough
|
||||
// `Math.ceil(days / 7)` leftover the API still requires — never what
|
||||
// the user actually asked for. The toast must report the days
|
||||
// figure, not `weeksWished`, once `targetDays` is set. `weeksWished`
|
||||
// is deliberately set to a nonsense value here (3) to prove the toast
|
||||
// isn't reading it at all, not just that it happens to agree.
|
||||
const created = makeLeaveWish({ id: 'lw-new', name: 'Ny period', targetDays: 7, targetWeeks: 1 })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
// isn't reading it at all, not just that it happens to agree. Driven
|
||||
// through a period-popover edit/save (band click), since no UI path
|
||||
// creates a fresh wish with `targetDays` anymore.
|
||||
const today = todayDateString()
|
||||
const wish = makeLeaveWish({ id: 'lw-1', name: 'Ny period', targetDays: 7, targetWeeks: 1 })
|
||||
const update = vi.fn().mockResolvedValue(wish)
|
||||
const sync = vi.fn().mockResolvedValue(
|
||||
makePlanSyncResponse({
|
||||
changes: [],
|
||||
proposal: makePlanProposal({
|
||||
outcomes: [
|
||||
makeWindowOutcome({
|
||||
wishId: 'lw-new',
|
||||
wishId: 'lw-1',
|
||||
startDate: '2026-09-17',
|
||||
endDate: '2026-09-23',
|
||||
weeksWished: 3,
|
||||
@@ -875,15 +894,17 @@ describe('Calendar', () => {
|
||||
}),
|
||||
}),
|
||||
)
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [] }))
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [wish], update }))
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
vi.mocked(usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
data: [makePlannedLeaveDay({ id: 'pd-1', date: today, wishId: 'lw-1', parentPersonalDataUuid: 'person-1' })],
|
||||
}),
|
||||
)
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[6] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
|
||||
fireEvent.click(screen.getByTestId('period-save'))
|
||||
|
||||
const toast = await screen.findByTestId('calendar-toast')
|
||||
expect(toast).toHaveTextContent('7')
|
||||
@@ -893,7 +914,7 @@ describe('Calendar', () => {
|
||||
})
|
||||
|
||||
it('shows no toast when a sync with no changes also has no outcome for the touched wish (genuine no-op)', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-new' })
|
||||
const created = makeLeaveWish({ id: 'lw-preset' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
const sync = vi.fn().mockResolvedValue(
|
||||
makePlanSyncResponse({
|
||||
@@ -905,26 +926,22 @@ describe('Calendar', () => {
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
fireEvent.click(screen.getByTestId('preset-click'))
|
||||
|
||||
await waitFor(() => expect(add).toHaveBeenCalled())
|
||||
expect(screen.queryByTestId('calendar-toast')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('reports both what changed and what fell short on a partially-funded sync', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-new', name: 'Sommar' })
|
||||
const created = makeLeaveWish({ id: 'lw-preset', name: 'Sommar' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
const sync = vi.fn().mockResolvedValue(
|
||||
makePlanSyncResponse({
|
||||
changes: [makePlanChange({ newWishId: 'lw-new' })],
|
||||
changes: [makePlanChange({ newWishId: 'lw-preset' })],
|
||||
proposal: makePlanProposal({
|
||||
outcomes: [
|
||||
makeWindowOutcome({
|
||||
wishId: 'lw-new',
|
||||
wishId: 'lw-preset',
|
||||
startDate: '2026-06-01',
|
||||
endDate: '2026-06-14',
|
||||
weeksWished: 2,
|
||||
@@ -940,11 +957,7 @@ describe('Calendar', () => {
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
fireEvent.click(screen.getByTestId('preset-click'))
|
||||
|
||||
const toast = await screen.findByTestId('calendar-toast')
|
||||
// What changed (existing "N day(s) changed" summary) ...
|
||||
@@ -955,7 +968,7 @@ describe('Calendar', () => {
|
||||
})
|
||||
|
||||
it('renders sync rejections through renderRejection, resolving the parent name and never a raw uuid', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-new' })
|
||||
const created = makeLeaveWish({ id: 'lw-preset' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
const sync = vi.fn().mockResolvedValue(
|
||||
makePlanSyncResponse({
|
||||
@@ -978,11 +991,7 @@ describe('Calendar', () => {
|
||||
)
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
fireEvent.click(screen.getByTestId('preset-click'))
|
||||
|
||||
const toast = await screen.findByTestId('calendar-toast')
|
||||
expect(toast).toHaveTextContent('Förälder A')
|
||||
@@ -1026,56 +1035,20 @@ describe('Calendar', () => {
|
||||
})
|
||||
|
||||
it('dismisses the toast on demand', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-new' })
|
||||
const created = makeLeaveWish({ id: 'lw-preset' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
const sync = vi.fn().mockResolvedValue(makePlanSyncResponse({ changes: [makePlanChange({ newWishId: 'lw-new' })] }))
|
||||
const sync = vi.fn().mockResolvedValue(makePlanSyncResponse({ changes: [makePlanChange({ newWishId: 'lw-preset' })] }))
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [created] }))
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
fireEvent.click(screen.getByTestId('preset-click'))
|
||||
|
||||
const dismissButton = await screen.findByTestId('calendar-toast-dismiss')
|
||||
fireEvent.click(dismissButton)
|
||||
expect(screen.queryByTestId('calendar-toast')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('undo after creating a period from a drag deletes the wish and re-syncs, leaving day cleanup to the backend', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-new' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
const remove = vi.fn().mockResolvedValue(undefined)
|
||||
const sync = vi.fn().mockResolvedValue(makePlanSyncResponse({ changes: [makePlanChange({ newWishId: 'lw-new' })] }))
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, remove, data: [created] }))
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
|
||||
// `LeaveWishService.delete()` deletes every `planned_leave_day` row
|
||||
// the wish produced as part of deleting the wish itself — the
|
||||
// frontend no longer snapshots or deletes days individually. The
|
||||
// outcome under test is therefore the delegation: undo removes the
|
||||
// wish and re-syncs, and never touches `plannedDaysHook.remove` at all.
|
||||
const removeDay = vi.fn().mockResolvedValue(undefined)
|
||||
vi.mocked(usePlannedDays).mockReturnValue(makePlannedDaysResult({ remove: removeDay }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
|
||||
const undoButton = await screen.findByTestId('calendar-toast-undo')
|
||||
const syncCallsBeforeUndo = sync.mock.calls.length
|
||||
fireEvent.click(undoButton)
|
||||
|
||||
await waitFor(() => expect(remove).toHaveBeenCalledWith('lw-new'))
|
||||
await waitFor(() => expect(sync.mock.calls.length).toBeGreaterThan(syncCallsBeforeUndo))
|
||||
expect(removeDay).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('undo after selecting a preset deletes the wish and re-syncs, leaving day cleanup to the backend', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-preset' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
@@ -1179,7 +1152,6 @@ describe('Calendar', () => {
|
||||
|
||||
expect(usePlannedDays).toHaveBeenCalledWith(undefined)
|
||||
expect(useCalendarDays).toHaveBeenCalledWith('', expect.any(Number), expect.any(Number))
|
||||
expect(useDayOverrides).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('treats a missing calendar window as having no days yet', () => {
|
||||
@@ -1277,70 +1249,6 @@ describe('Calendar', () => {
|
||||
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows a generic override error when the mutation rejects with a non-Error value', async () => {
|
||||
const today = todayDateString()
|
||||
const upsert = vi.fn().mockRejectedValue('network exploded')
|
||||
vi.mocked(useCalendarDays).mockReturnValue(
|
||||
makeCalendarDaysResult({
|
||||
data: makeCalendarWindow({ days: [makeCalendarDayEntry({ date: today, dayType: 'weekend', isFree: true })] }),
|
||||
}),
|
||||
)
|
||||
vi.mocked(useDayOverrides).mockReturnValue(makeDayOverridesResult({ data: [], upsert, remove: vi.fn() }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const todayCell = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith(today))
|
||||
fireEvent.click(todayCell!)
|
||||
fireEvent.click(screen.getByTestId('modal-toggle-override'))
|
||||
|
||||
await waitFor(() => {
|
||||
const attr = screen.getByTestId('modal').getAttribute('data-override-error')
|
||||
expect(attr).not.toBe('')
|
||||
expect(attr).not.toBe('network exploded')
|
||||
})
|
||||
})
|
||||
|
||||
it('treats a missing day-overrides list as empty when clearing an override', async () => {
|
||||
const today = todayDateString()
|
||||
const removeOverride = vi.fn()
|
||||
vi.mocked(useCalendarDays).mockReturnValue(
|
||||
makeCalendarDaysResult({
|
||||
data: makeCalendarWindow({
|
||||
days: [makeCalendarDayEntry({ date: today, isOverride: true, overrideIsWorkingDay: false, isFree: true })],
|
||||
}),
|
||||
}),
|
||||
)
|
||||
// `data` is undefined here (e.g. before the overrides fetch resolves) —
|
||||
// the `(dayOverridesHook.data ?? []).find(...)` fallback must kick in.
|
||||
vi.mocked(useDayOverrides).mockReturnValue({
|
||||
...makeDayOverridesResult({ isLoading: true, upsert: vi.fn(), remove: removeOverride }),
|
||||
data: undefined,
|
||||
} as unknown as ReturnType<typeof useDayOverrides>)
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const todayCell = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith(today))
|
||||
fireEvent.click(todayCell!)
|
||||
fireEvent.click(screen.getByTestId('modal-toggle-override'))
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('modal')).toHaveAttribute('data-override-error', ''))
|
||||
expect(removeOverride).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not attempt an override toggle when the workspace id is missing', () => {
|
||||
const upsert = vi.fn()
|
||||
vi.mocked(useWorkspace).mockReturnValue(makeWorkspaceContextValue({ workspace: null, personalDataUuid: 'person-1' }))
|
||||
vi.mocked(useDayOverrides).mockReturnValue(makeDayOverridesResult({ data: [], upsert, remove: vi.fn() }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDay = calendarDays.find((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.click(currentMonthDay!)
|
||||
fireEvent.click(screen.getByTestId('modal-toggle-override'))
|
||||
|
||||
expect(upsert).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows a generic error toast for a non-Error rejection while syncing', async () => {
|
||||
const sync = vi.fn().mockRejectedValue('boom')
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
@@ -1353,37 +1261,6 @@ describe('Calendar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('shows a generic error toast for a non-Error rejection when creating a period from a drag', async () => {
|
||||
const add = vi.fn().mockRejectedValue('boom')
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [] }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('calendar-toast')).toHaveTextContent('Det gick inte att skapa perioden'),
|
||||
)
|
||||
})
|
||||
|
||||
it('does not create a period from a drag when the workspace id is missing', () => {
|
||||
const add = vi.fn()
|
||||
vi.mocked(useWorkspace).mockReturnValue(makeWorkspaceContextValue({ workspace: null, personalDataUuid: null }))
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [] }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
|
||||
expect(add).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows a generic error toast for a non-Error rejection on undo', async () => {
|
||||
const today = todayDateString()
|
||||
const wish = makeLeaveWish({ id: 'lw-1' })
|
||||
@@ -1535,7 +1412,7 @@ describe('Calendar', () => {
|
||||
})
|
||||
|
||||
it('summarizes a change for a wish not present in the local wish list as a generic period', async () => {
|
||||
const created = makeLeaveWish({ id: 'lw-new' })
|
||||
const created = makeLeaveWish({ id: 'lw-preset' })
|
||||
const add = vi.fn().mockResolvedValue(created)
|
||||
const sync = vi.fn().mockResolvedValue(
|
||||
makePlanSyncResponse({ changes: [makePlanChange({ newWishId: 'some-other-wish-id' })] }),
|
||||
@@ -1545,13 +1422,53 @@ describe('Calendar', () => {
|
||||
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
|
||||
|
||||
render(<Calendar />)
|
||||
const calendarDays = screen.getAllByTestId('calendar-day')
|
||||
const currentMonthDays = calendarDays.filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
fireEvent.pointerDown(currentMonthDays[0] as HTMLElement)
|
||||
fireEvent.pointerEnter(currentMonthDays[2] as HTMLElement)
|
||||
fireEvent.pointerUp(window)
|
||||
fireEvent.click(screen.getByTestId('preset-click'))
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('calendar-toast')).toHaveTextContent('Perioden'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('§Task7 drag-select range save writes every dragged day', () => {
|
||||
it('writes every dragged date as its own planned day (add for new dates, update for existing ones), all pinned', async () => {
|
||||
// Render once first to read off the actual dates the drag will cover
|
||||
// (independent of which day of the month the suite happens to run
|
||||
// on), then re-render with a pre-existing planned day on the first
|
||||
// of those dates — the save must update it in place, not skip or
|
||||
// duplicate it.
|
||||
const { unmount } = render(<Calendar />)
|
||||
const probeDays = screen.getAllByTestId('calendar-day').filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
const firstDate = probeDays[0]!.getAttribute('aria-label')!.slice(0, 10)
|
||||
unmount()
|
||||
|
||||
const add = vi.fn().mockResolvedValue(makePlannedLeaveDay())
|
||||
const update = vi.fn().mockResolvedValue(makePlannedLeaveDay())
|
||||
vi.mocked(usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
data: [makePlannedLeaveDay({ id: 'pd-existing', date: firstDate, parentPersonalDataUuid: 'person-1' })],
|
||||
add,
|
||||
update,
|
||||
}),
|
||||
)
|
||||
|
||||
render(<Calendar />)
|
||||
const currentMonthDays = screen.getAllByTestId('calendar-day').filter((el) => !el.classList.contains('calendar-day--disabled'))
|
||||
const startDay = currentMonthDays[0] as HTMLElement
|
||||
const endDay = currentMonthDays[2] as HTMLElement
|
||||
|
||||
fireEvent.pointerDown(startDay)
|
||||
fireEvent.pointerEnter(endDay)
|
||||
fireEvent.pointerUp(window)
|
||||
|
||||
const modal = await screen.findByTestId('modal')
|
||||
expect(modal).toHaveAttribute('data-day-count', '3')
|
||||
fireEvent.click(screen.getByTestId('modal-save'))
|
||||
|
||||
// The pre-existing day is updated (pinned), the other two are added
|
||||
// (also pinned) — never a LeaveWish, never a sync.
|
||||
await waitFor(() => expect(update).toHaveBeenCalledWith('pd-existing', expect.objectContaining({ is_pinned: true })))
|
||||
await waitFor(() => expect(add).toHaveBeenCalledTimes(2))
|
||||
expect(add).toHaveBeenCalledWith(expect.objectContaining({ is_pinned: true }))
|
||||
await waitFor(() => expect(screen.queryByTestId('modal')).not.toBeInTheDocument())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user