feat: validate opening slots and format dates in French and English
Slot validation reports every problem at once rather than the first, so someone fixing a day's hours on a phone in the shop is not sent round the loop three times. It covers the format, the ordering, overlaps and the three-slot ceiling. Slots that merely touch are accepted: 13:00 closing and 13:00 reopening is pointless but not contradictory, and refusing it would only annoy whoever typed it. Month and day names are spelled out rather than taken from Intl. The screen text feeds a content hash that decides whether the e-ink panel redraws at all, so it has to be byte-stable and must not shift because a container image ships different ICU locale data. English uses the twelve-hour clock, as the payload contract specifies: "Closes at 6:30 pm". Noon and midnight are covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
formatDateEn,
|
||||
formatDateFr,
|
||||
formatHoursText,
|
||||
formatNextChangeEn,
|
||||
formatNextChangeFr,
|
||||
formatTimeEn,
|
||||
shortDayLabelEn,
|
||||
shortDayLabelFr,
|
||||
} from './format';
|
||||
|
||||
describe('formatDateFr / formatDateEn', () => {
|
||||
it('writes the day and month in full', () => {
|
||||
expect(formatDateFr('2026-09-20')).toBe('Dimanche 20 septembre');
|
||||
expect(formatDateEn('2026-09-20')).toBe('Sunday 20 September');
|
||||
});
|
||||
|
||||
it('does not pad the day number', () => {
|
||||
expect(formatDateFr('2026-09-01')).toBe('Mardi 1 septembre');
|
||||
expect(formatDateEn('2026-09-01')).toBe('Tuesday 1 September');
|
||||
});
|
||||
|
||||
it('handles every month', () => {
|
||||
expect(formatDateFr('2026-01-15')).toBe('Jeudi 15 janvier');
|
||||
expect(formatDateFr('2026-08-15')).toBe('Samedi 15 août');
|
||||
expect(formatDateFr('2026-12-25')).toBe('Vendredi 25 décembre');
|
||||
expect(formatDateEn('2026-12-25')).toBe('Friday 25 December');
|
||||
});
|
||||
});
|
||||
|
||||
describe('short day labels', () => {
|
||||
it('uses three letters, Monday first on the screen', () => {
|
||||
expect([1, 2, 3, 4, 5, 6, 0].map(shortDayLabelFr)).toEqual([
|
||||
'Lun',
|
||||
'Mar',
|
||||
'Mer',
|
||||
'Jeu',
|
||||
'Ven',
|
||||
'Sam',
|
||||
'Dim',
|
||||
]);
|
||||
expect([1, 2, 3, 4, 5, 6, 0].map(shortDayLabelEn)).toEqual([
|
||||
'Mon',
|
||||
'Tue',
|
||||
'Wed',
|
||||
'Thu',
|
||||
'Fri',
|
||||
'Sat',
|
||||
'Sun',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatHoursText', () => {
|
||||
it('renders a single slot', () => {
|
||||
expect(formatHoursText([{ open: '10:00', close: '18:30' }])).toBe('10:00 – 18:30');
|
||||
});
|
||||
|
||||
it('joins several slots with a middle dot', () => {
|
||||
expect(
|
||||
formatHoursText([
|
||||
{ open: '10:00', close: '13:00' },
|
||||
{ open: '14:00', close: '18:30' },
|
||||
]),
|
||||
).toBe('10:00 – 13:00 · 14:00 – 18:30');
|
||||
});
|
||||
|
||||
it('returns an empty string when the shop is closed', () => {
|
||||
expect(formatHoursText([])).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatTimeEn', () => {
|
||||
it('uses the twelve-hour clock', () => {
|
||||
expect(formatTimeEn('18:30')).toBe('6:30 pm');
|
||||
expect(formatTimeEn('10:00')).toBe('10:00 am');
|
||||
});
|
||||
|
||||
it('names noon and midnight correctly', () => {
|
||||
expect(formatTimeEn('12:00')).toBe('12:00 pm');
|
||||
expect(formatTimeEn('00:30')).toBe('12:30 am');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatNextChange', () => {
|
||||
it('announces today’s closing time', () => {
|
||||
expect(formatNextChangeFr({ kind: 'CLOSES', date: '2026-09-20', time: '18:30' }, '2026-09-20')).toBe(
|
||||
'Ferme à 18:30',
|
||||
);
|
||||
expect(formatNextChangeEn({ kind: 'CLOSES', date: '2026-09-20', time: '18:30' }, '2026-09-20')).toBe(
|
||||
'Closes at 6:30 pm',
|
||||
);
|
||||
});
|
||||
|
||||
it('announces a reopening later today', () => {
|
||||
expect(formatNextChangeFr({ kind: 'OPENS', date: '2026-09-20', time: '14:00' }, '2026-09-20')).toBe(
|
||||
'Ouvre à 14:00',
|
||||
);
|
||||
expect(formatNextChangeEn({ kind: 'OPENS', date: '2026-09-20', time: '14:00' }, '2026-09-20')).toBe(
|
||||
'Opens at 2:00 pm',
|
||||
);
|
||||
});
|
||||
|
||||
it('announces tomorrow by name', () => {
|
||||
expect(formatNextChangeFr({ kind: 'OPENS', date: '2026-09-21', time: '10:00' }, '2026-09-20')).toBe(
|
||||
'Ouvre demain à 10:00',
|
||||
);
|
||||
expect(formatNextChangeEn({ kind: 'OPENS', date: '2026-09-21', time: '10:00' }, '2026-09-20')).toBe(
|
||||
'Opens tomorrow at 10:00 am',
|
||||
);
|
||||
});
|
||||
|
||||
it('names the weekday when the reopening is further out', () => {
|
||||
expect(formatNextChangeFr({ kind: 'OPENS', date: '2026-09-22', time: '10:00' }, '2026-09-20')).toBe(
|
||||
'Ouvre mardi à 10:00',
|
||||
);
|
||||
expect(formatNextChangeEn({ kind: 'OPENS', date: '2026-09-22', time: '10:00' }, '2026-09-20')).toBe(
|
||||
'Opens Tuesday at 10:00 am',
|
||||
);
|
||||
});
|
||||
|
||||
it('says nothing when there is nothing to announce', () => {
|
||||
expect(formatNextChangeFr(null, '2026-09-20')).toBe('');
|
||||
expect(formatNextChangeEn(null, '2026-09-20')).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user