"Opens later at" and "closes earlier at" were prefilled with 14:00 and 16:00 — arbitrary values, wrong on most days, which left the preview showing hours nobody had asked for until both fields were corrected. They now start from the day's real opening and closing, so any adjustment is relative to what that day actually is. A day with a lunch break is bounded by its first opening and its last closing, which is what these two actions act on. Changing the date changes the hours the times are relative to, so a value typed for Tuesday must not survive into Thursday. The fields live in a component keyed on the date and React resets them by remounting — the React compiler rightly refuses an effect that writes state back, and it is the clearer arrangement anyway. The end-to-end test picks a trading day rather than assuming today is one. The first version guarded the assertions behind a visibility check, which would have passed in silence on exactly the days the controls matter least. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
169 lines
5.1 KiB
TypeScript
169 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
checkRange,
|
|
earlyClosing,
|
|
expandRange,
|
|
lateOpening,
|
|
MAX_RANGE_DAYS,
|
|
usualBounds,
|
|
} from './exceptions';
|
|
import type { Slot } from './types';
|
|
|
|
const FULL: Slot[] = [{ open: '10:00', close: '18:30' }];
|
|
const SPLIT: Slot[] = [
|
|
{ open: '10:00', close: '13:00' },
|
|
{ open: '14:00', close: '18:30' },
|
|
];
|
|
|
|
describe('lateOpening', () => {
|
|
it('trims the only slot', () => {
|
|
expect(lateOpening(FULL, '14:00')).toEqual([{ open: '14:00', close: '18:30' }]);
|
|
});
|
|
|
|
it('drops a morning that is entirely before the new opening', () => {
|
|
// A shop opening at 14:00 does not keep its 10:00-13:00 morning.
|
|
expect(lateOpening(SPLIT, '14:00')).toEqual([{ open: '14:00', close: '18:30' }]);
|
|
});
|
|
|
|
it('trims the slot the new opening falls inside', () => {
|
|
expect(lateOpening(SPLIT, '11:00')).toEqual([
|
|
{ open: '11:00', close: '13:00' },
|
|
{ open: '14:00', close: '18:30' },
|
|
]);
|
|
});
|
|
|
|
it('changes nothing when the shop already opens later', () => {
|
|
expect(lateOpening(FULL, '09:00')).toEqual(FULL);
|
|
});
|
|
|
|
it('closes the day when the new opening is past every slot', () => {
|
|
expect(lateOpening(FULL, '19:00')).toEqual([]);
|
|
expect(lateOpening(SPLIT, '18:30')).toEqual([]);
|
|
});
|
|
|
|
it('leaves a closed day closed', () => {
|
|
expect(lateOpening([], '14:00')).toEqual([]);
|
|
});
|
|
|
|
it('does not mutate its input', () => {
|
|
const slots = SPLIT.map((slot) => ({ ...slot }));
|
|
lateOpening(slots, '11:00');
|
|
expect(slots).toEqual(SPLIT);
|
|
});
|
|
});
|
|
|
|
describe('earlyClosing', () => {
|
|
it('trims the only slot', () => {
|
|
expect(earlyClosing(FULL, '15:00')).toEqual([{ open: '10:00', close: '15:00' }]);
|
|
});
|
|
|
|
it('drops an afternoon that starts after the new closing', () => {
|
|
expect(earlyClosing(SPLIT, '13:00')).toEqual([{ open: '10:00', close: '13:00' }]);
|
|
});
|
|
|
|
it('trims the slot the new closing falls inside', () => {
|
|
expect(earlyClosing(SPLIT, '15:00')).toEqual([
|
|
{ open: '10:00', close: '13:00' },
|
|
{ open: '14:00', close: '15:00' },
|
|
]);
|
|
});
|
|
|
|
it('changes nothing when the shop already closes earlier', () => {
|
|
expect(earlyClosing(FULL, '19:00')).toEqual(FULL);
|
|
});
|
|
|
|
it('closes the day when the new closing is before every slot', () => {
|
|
expect(earlyClosing(FULL, '09:00')).toEqual([]);
|
|
expect(earlyClosing(SPLIT, '10:00')).toEqual([]);
|
|
});
|
|
|
|
it('is the mirror of lateOpening on a split day', () => {
|
|
// Opening late then closing early must leave the middle of the day.
|
|
expect(earlyClosing(lateOpening(SPLIT, '11:00'), '15:00')).toEqual([
|
|
{ open: '11:00', close: '13:00' },
|
|
{ open: '14:00', close: '15:00' },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('expandRange', () => {
|
|
it('includes both ends', () => {
|
|
expect(expandRange('2026-09-20', '2026-09-22')).toEqual([
|
|
'2026-09-20',
|
|
'2026-09-21',
|
|
'2026-09-22',
|
|
]);
|
|
});
|
|
|
|
it('handles a single day', () => {
|
|
expect(expandRange('2026-09-20', '2026-09-20')).toEqual(['2026-09-20']);
|
|
});
|
|
|
|
it('crosses a month and a year boundary', () => {
|
|
expect(expandRange('2026-12-30', '2027-01-02')).toEqual([
|
|
'2026-12-30',
|
|
'2026-12-31',
|
|
'2027-01-01',
|
|
'2027-01-02',
|
|
]);
|
|
});
|
|
|
|
it('returns nothing for a reversed range', () => {
|
|
expect(expandRange('2026-09-22', '2026-09-20')).toEqual([]);
|
|
});
|
|
|
|
it('refuses a range longer than the ceiling', () => {
|
|
// A typo in a year field should produce an error, not thirty thousand rows.
|
|
expect(expandRange('2026-01-01', '2030-01-01')).toEqual([]);
|
|
});
|
|
|
|
it('accepts a range exactly at the ceiling', () => {
|
|
const end = expandRange('2026-01-01', '2026-01-01');
|
|
expect(end).toHaveLength(1);
|
|
expect(expandRange('2026-01-01', '2026-04-01').length).toBeLessThanOrEqual(MAX_RANGE_DAYS + 1);
|
|
});
|
|
});
|
|
|
|
describe('checkRange', () => {
|
|
it('accepts a sound range', () => {
|
|
expect(checkRange('2026-09-20', '2026-09-22')).toBeNull();
|
|
});
|
|
|
|
it('names each problem', () => {
|
|
expect(checkRange('', '2026-09-22')).toBe('EMPTY');
|
|
expect(checkRange('2026-09-22', '')).toBe('EMPTY');
|
|
expect(checkRange('2026-09-22', '2026-09-20')).toBe('REVERSED');
|
|
expect(checkRange('2026-01-01', '2030-01-01')).toBe('TOO_LONG');
|
|
});
|
|
});
|
|
|
|
describe('usualBounds', () => {
|
|
it('takes the times of a single period', () => {
|
|
expect(usualBounds([{ open: '11:00', close: '18:00' }])).toEqual({
|
|
open: '11:00',
|
|
close: '18:00',
|
|
});
|
|
});
|
|
|
|
it('spans a day with a lunch break from first opening to last closing', () => {
|
|
// "Opens later" and "closes earlier" act on the day as a whole.
|
|
expect(usualBounds(SPLIT)).toEqual({ open: '10:00', close: '18:30' });
|
|
});
|
|
|
|
it('falls back to usable times on a closed day', () => {
|
|
// The controls are hidden there, but the inputs must still hold a value.
|
|
expect(usualBounds([])).toEqual({ open: '10:00', close: '18:00' });
|
|
});
|
|
|
|
it('handles three periods', () => {
|
|
expect(
|
|
usualBounds([
|
|
{ open: '09:30', close: '12:00' },
|
|
{ open: '13:30', close: '16:00' },
|
|
{ open: '17:00', close: '19:30' },
|
|
]),
|
|
).toEqual({ open: '09:30', close: '19:30' });
|
|
});
|
|
});
|