import React, { useState } from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import dayjs from 'dayjs'; jest.mock('lucide-react', () => ({ Calendar: () => , ChevronLeft: () => , ChevronRight: () => , ChevronDown: () => , })); import { DatePicker } from '@/components/ds/DatePicker'; import { toDateRange } from '@/lib/dateRange'; /** * What the picker hands its consumer, for each control. * * "This Month is not even responding" was the report. It emits `{from, to}`, * not a date string — and the Deliveries page forced it through * `dayjs().format()`, which is Invalid Date for an object. Worse, the handler * called a setter that did not exist, and the ReferenceError propagated out of * `commit` BEFORE `setOpen(false)`, so the popover never even closed. Clicking * the preset genuinely did nothing visible. */ // Radix Popover needs these in jsdom. beforeAll(() => { if (!window.ResizeObserver) { window.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} }; } if (!Element.prototype.hasPointerCapture) { Element.prototype.hasPointerCapture = () => false; Element.prototype.setPointerCapture = () => {}; Element.prototype.releasePointerCapture = () => {}; } if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = () => {}; }); const today = dayjs().startOf('day'); const todayStr = today.format('YYYY-MM-DD'); // The collapsed picker renders three buttons — prev-day, the trigger, // next-day — so the trigger is selected by its popup role, not by position. const openCalendar = () => fireEvent.click(document.querySelector('[aria-haspopup="dialog"]')); const openPicker = (onChange, value = todayStr) => { render(); openCalendar(); }; describe('DatePicker — what each control emits', () => { it('This Month emits a RANGE object, not a date string', () => { const onChange = jest.fn(); openPicker(onChange); fireEvent.click(screen.getByText('This Month')); expect(onChange).toHaveBeenCalledTimes(1); const emitted = onChange.mock.calls[0][0]; expect(typeof emitted).toBe('object'); expect(emitted).toEqual({ from: today.startOf('month').format('YYYY-MM-DD'), to: todayStr }); }); it('Last 7 Days emits a range covering today and the six before it', () => { const onChange = jest.fn(); openPicker(onChange); fireEvent.click(screen.getByText('Last 7 Days')); expect(onChange).toHaveBeenCalledWith({ from: today.subtract(6, 'day').format('YYYY-MM-DD'), to: todayStr }); }); it('All Dates emits the string "all", which is not a date', () => { const onChange = jest.fn(); openPicker(onChange); fireEvent.click(screen.getByText('All Dates')); expect(onChange).toHaveBeenCalledWith('all'); }); it('Yesterday emits a plain YYYY-MM-DD string', () => { const onChange = jest.fn(); openPicker(onChange); fireEvent.click(screen.getByText('Yesterday')); expect(onChange).toHaveBeenCalledWith(today.subtract(1, 'day').format('YYYY-MM-DD')); }); it('every emitted shape survives toDateRange with real bounds', () => { // The contract the Deliveries page depends on: whatever the picker emits, // the page can turn it into usable bounds without inspecting its type. const emitted = []; const onChange = (v) => emitted.push(v); ['This Month', 'Last 7 Days', 'Yesterday'].forEach((label) => { const { unmount } = render(); openCalendar(); fireEvent.click(screen.getByText(label)); unmount(); }); expect(emitted).toHaveLength(3); emitted.forEach((v) => { const { from, to } = toDateRange(v); expect(from).toMatch(/^\d{4}-\d{2}-\d{2}$/); expect(to).toMatch(/^\d{4}-\d{2}-\d{2}$/); expect(from <= to).toBe(true); }); }); }); describe('DatePicker — the popover after a choice', () => { it('closes once a preset is chosen', () => { const onChange = jest.fn(); openPicker(onChange); expect(screen.getByText('This Month')).toBeInTheDocument(); fireEvent.click(screen.getByText('This Month')); expect(screen.queryByText('This Month')).not.toBeInTheDocument(); }); }); /** * rangeSelect (Orders Summary): the first calendar click used to commit a * single day and close the popover, so an operator could never pick a from * AND a to date. With rangeSelect the first click only marks the start. */ describe('DatePicker — rangeSelect', () => { // A month well in the past, so every day in it is selectable under maxDate. const month = today.subtract(2, 'month').startOf('month'); const day = (n) => month.date(n).format('YYYY-MM-DD'); const dayButton = (n) => screen.getAllByRole('button', { name: String(n) }).find((b) => !b.className.includes('text-ink-4/40')); const openRangePicker = (onChange, extra = {}) => { render(); openCalendar(); }; it('keeps the calendar open after the first click and emits nothing yet', () => { const onChange = jest.fn(); openRangePicker(onChange); fireEvent.click(dayButton(5)); expect(onChange).not.toHaveBeenCalled(); expect(screen.getByText(/now pick the end date/i)).toBeInTheDocument(); }); it('emits { from, to } on the second click and closes', () => { const onChange = jest.fn(); openRangePicker(onChange); fireEvent.click(dayButton(5)); fireEvent.click(dayButton(12)); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith({ from: day(5), to: day(12) }); expect(screen.queryByText(/pick the end date/i)).not.toBeInTheDocument(); }); it('orders the two clicks when the end is picked before the start', () => { const onChange = jest.fn(); openRangePicker(onChange); fireEvent.click(dayButton(20)); fireEvent.click(dayButton(3)); expect(onChange).toHaveBeenCalledWith({ from: day(3), to: day(20) }); }); it('greys out and ignores days after maxDate', () => { const onChange = jest.fn(); render(); openCalendar(); const tomorrow = today.add(1, 'day'); // Only assert when tomorrow is visible in this month's grid. const btn = screen .getAllByRole('button', { name: String(tomorrow.date()) }) .find((b) => b.disabled); if (tomorrow.month() === today.month()) { expect(btn).toBeDefined(); fireEvent.click(btn); fireEvent.click(btn); expect(onChange).not.toHaveBeenCalled(); } }); it('leaves the default (single-click) pickers unchanged', () => { const onChange = jest.fn(); render(); openCalendar(); fireEvent.click(screen.getByText('Yesterday')); expect(onChange).toHaveBeenCalledWith(today.subtract(1, 'day').format('YYYY-MM-DD')); }); });