import { fireEvent, render, screen } from '@testing-library/react'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { DatePicker, DatePickerInputSimple } from '../date-picker'
/**
* `fromDate` / `toDate` are the pickers' only bounds, and they have already been
* dropped silently once: react-day-picker **v9** removed the props of those
* names, so the calendar kept forwarding two values the library no longer read
* and every consumer's minimum/maximum quietly stopped applying. Nothing failed
* — the picker simply accepted any date — which is exactly the kind of
* regression a type check cannot see and a test has to.
*
* Each case asserts BOTH halves of a bound: the out-of-range day is refused and
* an in-range one is still accepted. A matcher that disabled the whole month
* would satisfy the first assertion on its own.
*/
beforeAll(() => {
// jsdom ships no matchMedia; the calendar reads `useMdUp` to decide how many
// months to show. "No match" pins it to the single-month layout these
// assertions are written against.
window.matchMedia = ((query: string) => ({
matches: false,
media: query,
onchange: null,
addEventListener: () => {},
removeEventListener: () => {},
addListener: () => {},
removeListener: () => {},
dispatchEvent: () => false,
})) as unknown as typeof window.matchMedia
})
/** Local midnight, so no assertion straddles a day boundary. */
function day(year: number, month: number, date: number): Date {
return new Date(year, month, date)
}
/**
* The day cell for an ISO date, and the button inside it.
*
* Found through `data-day` rather than by role or text: v9 gives the cell the
* ISO date and every day button the same accessible name shape ("Friday, June
* 5th, 2026"), so a query by the day NUMBER matches the wrong month's outside
* days as well. The calendar renders into a portal, hence `document`.
*/
function dayButton(iso: string): HTMLButtonElement {
const cell = document.querySelector(`[data-day="${iso}"]`)
expect(cell, `no day cell for ${iso}`).not.toBeNull()
const button = cell?.querySelector('button')
expect(button, `no day button for ${iso}`).not.toBeNull()
return button as HTMLButtonElement
}
/** Opens the picker's popover — the trigger shows the current value. */
function openCalendar(): void {
fireEvent.click(screen.getByRole('button', { name: /06\/15\/2026/ }))
}
describe('date picker bounds', () => {
it('refuses a day before fromDate and still takes one after it', () => {
const onChange = vi.fn()
render(
)
openCalendar()
fireEvent.click(dayButton('2026-06-05'))
expect(onChange).not.toHaveBeenCalled()
fireEvent.click(dayButton('2026-06-20'))
expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange.mock.calls[0][0]?.getDate()).toBe(20)
})
it('refuses a day after toDate and still takes one before it', () => {
const onChange = vi.fn()
render(
)
openCalendar()
fireEvent.click(dayButton('2026-06-25'))
expect(onChange).not.toHaveBeenCalled()
fireEvent.click(dayButton('2026-06-18'))
expect(onChange).toHaveBeenCalledTimes(1)
})
it('marks the out-of-range day disabled instead of removing it', () => {
render()
openCalendar()
// The day keeps its place in the month grid — a calendar missing its first
// week reads as broken, and the greyed cell is what shows the bound exists.
expect(dayButton('2026-06-05')).toBeDisabled()
expect(dayButton('2026-06-20')).not.toBeDisabled()
expect(document.querySelector('[data-day="2026-06-05"]')).toHaveAttribute('data-disabled', 'true')
})
it('stops month navigation at the bound', () => {
render()
openCalendar()
// June still holds selectable days, so it stays reachable — but there is
// nothing to select before it.
expect(screen.getByRole('button', { name: 'Previous month' })).toBeDisabled()
expect(screen.getByRole('button', { name: 'Next month' })).not.toBeDisabled()
})
it('leaves navigation and selection unbounded when no bounds are given', () => {
const onChange = vi.fn()
render()
openCalendar()
expect(screen.getByRole('button', { name: 'Previous month' })).not.toBeDisabled()
fireEvent.click(dayButton('2026-06-01'))
expect(onChange).toHaveBeenCalledTimes(1)
})
})