Files
ita-ito-horaires/lib/schedule/validate.test.ts
T
vliaudatandClaude Opus 5 399a353284 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
2026-09-20 17:39:26 +02:00

87 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { MAX_SLOTS_PER_DAY, validateSlots } from './validate';
describe('validateSlots', () => {
it('accepts a single slot', () => {
expect(validateSlots([{ open: '10:00', close: '18:30' }])).toEqual([]);
});
it('accepts a lunch break', () => {
expect(
validateSlots([
{ open: '10:00', close: '13:00' },
{ open: '14:00', close: '18:30' },
]),
).toEqual([]);
});
it('accepts an empty list, which simply means closed', () => {
expect(validateSlots([])).toEqual([]);
});
it('rejects a malformed time', () => {
expect(validateSlots([{ open: '10h00', close: '18:30' }])).toContain(
'Heure invalide : « 10h00 ». Format attendu : HH:mm.',
);
});
it('rejects an hour outside the clock', () => {
expect(validateSlots([{ open: '25:00', close: '26:00' }]).length).toBeGreaterThan(0);
expect(validateSlots([{ open: '10:60', close: '18:00' }]).length).toBeGreaterThan(0);
});
it('rejects a slot that closes before it opens', () => {
expect(validateSlots([{ open: '18:30', close: '10:00' }])).toContain(
"L'heure de fermeture doit suivre l'heure d'ouverture : 18:30 – 10:00.",
);
});
it('rejects a slot of zero length', () => {
expect(validateSlots([{ open: '10:00', close: '10:00' }]).length).toBeGreaterThan(0);
});
it('rejects overlapping slots', () => {
expect(
validateSlots([
{ open: '10:00', close: '14:00' },
{ open: '13:00', close: '18:30' },
]),
).toContain('Les plages 10:00 – 14:00 et 13:00 – 18:30 se chevauchent.');
});
it('detects an overlap even when the slots are given out of order', () => {
expect(
validateSlots([
{ open: '13:00', close: '18:30' },
{ open: '10:00', close: '14:00' },
]).length,
).toBeGreaterThan(0);
});
it('accepts slots that merely touch', () => {
// 13:00 closing and 13:00 reopening is pointless but not contradictory,
// and refusing it would only annoy whoever typed it.
expect(
validateSlots([
{ open: '10:00', close: '13:00' },
{ open: '13:00', close: '18:30' },
]),
).toEqual([]);
});
it('rejects more slots than a shop day can hold', () => {
const tooMany = Array.from({ length: MAX_SLOTS_PER_DAY + 1 }, (_, index) => ({
open: `${(8 + index * 2).toString().padStart(2, '0')}:00`,
close: `${(9 + index * 2).toString().padStart(2, '0')}:00`,
}));
expect(validateSlots(tooMany)).toContain(
`Trois plages horaires au maximum par jour (${tooMany.length} fournies).`,
);
});
it('reports several problems at once', () => {
expect(validateSlots([{ open: 'nope', close: '10:00' }]).length).toBeGreaterThan(0);
});
});