120 lines
4.9 KiB
JavaScript
120 lines
4.9 KiB
JavaScript
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(<DateRangeFields value={value} onChange={onChange} />);
|
|
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(<DateRangeFields value={value} onChange={onChange} />);
|
|
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(<DateRangeFields value={{ from: today, to: today }} onChange={onChange} max={null} />);
|
|
const from = screen.getByLabelText('From date');
|
|
fireEvent.change(from, { target: { value: nextYear } });
|
|
expect(onChange).toHaveBeenCalledWith({ from: nextYear, to: nextYear });
|
|
});
|
|
});
|