import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { describe, it, expect, vi } from 'vitest' import { axe } from 'vitest-axe' import * as React from 'react' import { DatePicker, type DateRange } from './date-picker' const JUN_10 = new Date(2026, 5, 10) const JUN_17 = new Date(2026, 5, 17) function getDateSegments() { return screen.getAllByRole('spinbutton', { name: /day|month|year/ }) } function getMonthSegment() { return screen.getByRole('spinbutton', { name: 'month' }) } describe('DatePicker', () => { describe('single mode', () => { it('renders a DateField populated from defaultValue', () => { render() expect(screen.getByRole('spinbutton', { name: 'month' })).toHaveTextContent('06') expect(screen.getByRole('spinbutton', { name: 'day' })).toHaveTextContent('10') expect(screen.getByRole('spinbutton', { name: 'year' })).toHaveTextContent('2026') }) it('opens the calendar popover when the trigger button is clicked', async () => { const user = userEvent.setup() render() expect(screen.queryByRole('grid')).not.toBeInTheDocument() await user.click(screen.getByRole('button', { name: /open calendar/i })) expect(await screen.findByRole('grid')).toBeInTheDocument() }) it('focusing a segment does NOT open the popover', async () => { const user = userEvent.setup() render() await user.click(getMonthSegment()) expect(screen.queryByRole('grid')).not.toBeInTheDocument() }) it('selecting a day in the calendar updates the field and closes the popover', async () => { const user = userEvent.setup() const onValueChange = vi.fn() render() await user.click(screen.getByRole('button', { name: /open calendar/i })) const dayBtn = await screen.findByRole('button', { name: /June 10th, 2026/ }) await user.click(dayBtn) expect(onValueChange).toHaveBeenCalled() const [picked] = onValueChange.mock.calls[0]! expect(picked).toBeInstanceOf(Date) expect((picked as Date).getDate()).toBe(10) }) it('renders a TimeField alongside DateField when showTime is enabled', () => { render() // 3 date segments + 3 time segments (HH:mm:ss) const segments = screen.getAllByRole('spinbutton') expect(segments.length).toBeGreaterThanOrEqual(6) const hour = screen.getByRole('spinbutton', { name: 'hour' }) const minute = screen.getByRole('spinbutton', { name: 'minute' }) expect(hour).toHaveTextContent('09') expect(minute).toHaveTextContent('30') }) it('typing an intermediate-invalid digit (day=0) does not clear the picker value', async () => { const user = userEvent.setup() const onValueChange = vi.fn() function Controlled() { const [date, setDate] = React.useState(JUN_10) return ( { setDate(v) onValueChange(v) }} defaultMonth={JUN_10} /> ) } render() onValueChange.mockClear() await user.click(screen.getByRole('spinbutton', { name: 'day' })) await user.keyboard('0') // Picker value must remain Jun 10; segments stay editable. expect(onValueChange).not.toHaveBeenCalled() expect(screen.getByRole('spinbutton', { name: 'month' })).toHaveTextContent('06') expect(screen.getByRole('spinbutton', { name: 'year' })).toHaveTextContent('2026') }) it('opening the calendar after a completed field edit shows the new month', async () => { const user = userEvent.setup() function Controlled() { const [date, setDate] = React.useState(JUN_10) return } render() // Replace the month with September (09) and reopen the calendar. const monthSeg = screen.getByRole('spinbutton', { name: 'month' }) await user.click(monthSeg) await user.keyboard('09') await user.click(screen.getByRole('button', { name: /open calendar/i })) // react-day-picker labels the month caption - September should now be visible. const grid = await screen.findByRole('grid') expect(grid).toHaveAccessibleName(/september 2026/i) }) it('selecting a day preserves the existing time portion of the value', async () => { const user = userEvent.setup() const onValueChange = vi.fn() render( , ) await user.click(screen.getByRole('button', { name: /open calendar/i })) const dayBtn = await screen.findByRole('button', { name: /June 10th, 2026/ }) await user.click(dayBtn) const [picked] = onValueChange.mock.calls[0]! const d = picked as Date expect(d.getDate()).toBe(10) expect(d.getHours()).toBe(14) expect(d.getMinutes()).toBe(30) }) }) describe('range mode', () => { it('renders two DateFields with a separator', () => { const range: DateRange = { from: JUN_10, to: JUN_17 } render() const segments = getDateSegments() // two DateFields = 6 segments (day, month, year × 2) expect(segments).toHaveLength(6) }) it('updates only the from half when the from field changes', () => { const onValueChange = vi.fn() function Controlled() { const [range, setRange] = React.useState({ from: JUN_10, to: JUN_17 }) return ( { setRange(next) onValueChange(next) }} /> ) } render() // Sanity: both halves rendered const dayCells = screen.getAllByRole('spinbutton', { name: 'day' }) expect(dayCells).toHaveLength(2) }) it('does NOT auto-close the popover after both ends are picked', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('button', { name: /open calendar/i })) const grid = await screen.findByRole('grid') await user.click(await screen.findByRole('button', { name: /June 10th, 2026/ })) await user.click(await screen.findByRole('button', { name: /June 17th, 2026/ })) // Popover stays open so the range can still be refined; it dismisses on outside-click/Escape. expect(grid).toBeInTheDocument() }) }) describe('multiple mode', () => { it('renders a read-only summary input', () => { const days = [JUN_10, JUN_17, new Date(2026, 5, 24)] render() const input = screen.getByRole('textbox') as HTMLInputElement expect(input).toHaveAttribute('readonly') expect(input.value).toMatch(/06\/10\/2026 \(\+2 more\)/) }) it('does NOT auto-close the popover when picking days', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('button', { name: /open calendar/i })) const grid = await screen.findByRole('grid') const dayBtn = await screen.findByRole('button', { name: /June 10th, 2026/ }) await user.click(dayBtn) // Popover should still be open after selecting a day in multiple mode expect(grid).toBeInTheDocument() }) it('clearable empties the summary and clears the value', async () => { const user = userEvent.setup() const onValueChange = vi.fn() render( , ) const clearBtn = screen.getByRole('button', { name: /clear/i }) await user.click(clearBtn) expect(onValueChange).toHaveBeenCalledWith(undefined) }) it('one click on clear empties the input in a controlled multiple-mode picker', async () => { const user = userEvent.setup() function Controlled() { const [days, setDays] = React.useState([JUN_10, JUN_17, new Date(2026, 5, 24)]) return } render() const input = screen.getByRole('textbox') as HTMLInputElement expect(input.value).toMatch(/\(\+2 more\)/) await user.click(screen.getByRole('button', { name: /clear/i })) expect(input.value).toBe('') }) it('clears in one click immediately after picking dates with popover still open', async () => { const user = userEvent.setup() const onValueChange = vi.fn() function Controlled() { const [days, setDays] = React.useState(undefined) return ( { onValueChange(next) setDays(next) }} /> ) } render() await user.click(screen.getByRole('button', { name: /open calendar/i })) await user.click(await screen.findByRole('button', { name: /June 10th, 2026/ })) await user.click(await screen.findByRole('button', { name: /June 17th, 2026/ })) await user.click(await screen.findByRole('button', { name: /June 24th, 2026/ })) const input = screen.getByRole('textbox') as HTMLInputElement expect(input.value).toMatch(/\(\+2 more\)/) onValueChange.mockClear() // Popover is still open at this point. await user.click(screen.getByRole('button', { name: /clear/i })) expect(input.value).toBe('') }) it('one click on clear empties a single-date controlled multiple-mode picker', async () => { const user = userEvent.setup() function Controlled() { const [days, setDays] = React.useState([JUN_10]) return } render() const input = screen.getByRole('textbox') as HTMLInputElement expect(input.value).toBe('06/10/2026') await user.click(screen.getByRole('button', { name: /clear/i })) expect(input.value).toBe('') }) }) it('inherits invalid and disabled from a surrounding Field', async () => { const { Field } = await import('@base-ui/react/field') const { container } = render( , ) const root = container.querySelector('[data-slot="date-picker"]') expect(root).toHaveAttribute('data-invalid') expect(root).toHaveAttribute('data-disabled') }) it('has no accessibility violations', async () => { const { container } = render() expect(await axe(container)).toHaveNoViolations() }) })