194 lines
7.1 KiB
JavaScript
194 lines
7.1 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import dayjs from 'dayjs';
|
|
|
|
jest.mock('lucide-react', () => ({
|
|
Calendar: () => <span data-testid="icon-calendar" />,
|
|
ChevronLeft: () => <span data-testid="icon-left" />,
|
|
ChevronRight: () => <span data-testid="icon-right" />,
|
|
ChevronDown: () => <span data-testid="icon-down" />,
|
|
}));
|
|
|
|
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(<DatePicker value={value} onChange={onChange} />);
|
|
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(<DatePicker value={todayStr} onChange={onChange} />);
|
|
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(<DatePicker value={{ from: day(1), to: day(1) }} onChange={onChange} rangeSelect maxDate={todayStr} {...extra} />);
|
|
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(<DatePicker value={todayStr} onChange={onChange} rangeSelect maxDate={todayStr} />);
|
|
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(<DatePicker value={todayStr} onChange={onChange} />);
|
|
openCalendar();
|
|
fireEvent.click(screen.getByText('Yesterday'));
|
|
expect(onChange).toHaveBeenCalledWith(today.subtract(1, 'day').format('YYYY-MM-DD'));
|
|
});
|
|
});
|