import { describe, expect, it } from 'vitest'; import { checkRange, earlyClosing, expandRange, lateOpening, MAX_RANGE_DAYS } 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'); }); });