import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import dayjs from 'dayjs'; import { DateRangeFields } from '@/components/ds/DateRangeFields'; /** * The two ranges that can only ever return nothing. * * Both were accepted silently by the report pages before this component * existed: an inverted range (from 30 Sep, to 1 Sep) and a future one. Neither * input reported invalid, no message appeared, and the table showed zero rows — * which an operator reads as "no orders in this period" rather than "this * filter is impossible". An empty result presented as a finding is the same * class of defect as a silently truncated count. * * `min`/`max` cover the picker, which is how it is actually used. The clamp in * onChange covers typing and pasting, which ignore those attributes — so both * are asserted here. */ const today = dayjs().format('YYYY-MM-DD'); const yesterday = dayjs().subtract(1, 'day').format('YYYY-MM-DD'); const lastWeek = dayjs().subtract(7, 'day').format('YYYY-MM-DD'); const nextYear = dayjs().add(1, 'year').format('YYYY-MM-DD'); const setup = (value) => { const onChange = jest.fn(); render(); return { onChange, from: screen.getByLabelText('From date'), to: screen.getByLabelText('To date') }; }; describe('bounds the picker itself', () => { it('neither field offers a day after today', () => { const { from, to } = setup({ from: lastWeek, to: yesterday }); expect(to).toHaveAttribute('max', today); // from is capped by the to-date, which is tighter than today here expect(from).toHaveAttribute('max', yesterday); }); it('the to-field cannot go earlier than the from-field', () => { const { to } = setup({ from: lastWeek, to: today }); expect(to).toHaveAttribute('min', lastWeek); }); it('with no to-date yet, from is capped at today', () => { const { from } = setup({ from: '', to: '' }); expect(from).toHaveAttribute('max', today); }); }); describe('clamps what the picker cannot — typing and pasting', () => { it('a future from-date is pulled back to today', () => { const { from, onChange } = setup({ from: today, to: today }); fireEvent.change(from, { target: { value: nextYear } }); expect(onChange).toHaveBeenCalledWith({ from: today, to: today }); }); it('a future to-date is pulled back to today', () => { const { to, onChange } = setup({ from: lastWeek, to: today }); fireEvent.change(to, { target: { value: nextYear } }); expect(onChange).toHaveBeenCalledWith({ from: lastWeek, to: today }); }); it('moving from PAST to drags to along, rather than inverting', () => { // The edit is honoured — you have moved to a later period — and the other // end follows. Rejecting the keystroke with no explanation is worse. const { from, onChange } = setup({ from: lastWeek, to: yesterday }); fireEvent.change(from, { target: { value: today } }); expect(onChange).toHaveBeenCalledWith({ from: today, to: today }); }); it('moving to BEFORE from drags from back, rather than inverting', () => { const { to, onChange } = setup({ from: yesterday, to: today }); fireEvent.change(to, { target: { value: lastWeek } }); expect(onChange).toHaveBeenCalledWith({ from: lastWeek, to: lastWeek }); }); it('never emits a range where from is after to', () => { // The property, stated once. Every emission from any edit must hold it. const cases = [ [{ from: lastWeek, to: yesterday }, 'From date', today], [{ from: yesterday, to: today }, 'To date', lastWeek], [{ from: today, to: today }, 'From date', nextYear], [{ from: lastWeek, to: today }, 'To date', nextYear] ]; cases.forEach(([value, label, typed]) => { const onChange = jest.fn(); const { unmount } = render(); fireEvent.change(screen.getByLabelText(label), { target: { value: typed } }); const next = onChange.mock.calls[0][0]; expect(next.from <= next.to).toBe(true); expect(next.to <= today).toBe(true); unmount(); }); }); }); describe('a cleared field is not an invalid one', () => { it('passes an empty value through untouched', () => { const { to, onChange } = setup({ from: lastWeek, to: today }); fireEvent.change(to, { target: { value: '' } }); expect(onChange).toHaveBeenCalledWith({ from: lastWeek, to: '' }); }); }); describe('the ceiling is overridable', () => { it('max={null} allows a future date, for a picker that should look ahead', () => { const onChange = jest.fn(); render(); const from = screen.getByLabelText('From date'); fireEvent.change(from, { target: { value: nextYear } }); expect(onChange).toHaveBeenCalledWith({ from: nextYear, to: nextYear }); }); });