Édition
- Le panneau de créneau sert aussi à la modification : déplacer un créneau,
c'est changer son jour, son salarié ou ses heures — exactement les champs de
la création. Deux écrans distincts finiraient par diverger.
- Duplication d'une semaine vers une autre, refusée si la destination contient
déjà des créneaux. Le report se fait **par heure locale**, pas par décalage de
sept jours : entre mars et avril, sept jours d'écart ne redonnent pas la même
heure.
Vues
- `/planning/etiquettes` : la semaine par poste. C'est la question du
responsable d'ouverture — « la caisse est-elle tenue samedi ? » — illisible
sur une grille dont les lignes sont des personnes. Les postes sans créneau
restent affichés : un poste vide toute la semaine est une information.
- `/planning/mois` : les heures par salarié et par jour, avec les journées de
plus de 10 h signalées. Vue de contrôle, pas d'édition.
- Les deux lisent `Shift` directement. Un test e2e pose un créneau et le
retrouve identique dans les quatre vues — c'est ce qui empêche l'une d'elles
de dériver vers son propre calcul.
Impression
- Feuille d'impression A4 paysage : les zones à défilement se déplient, les
lignes ne se coupent pas entre deux pages, et les aplats de poste sortent
(`print-color-adjust: exact`) sans être nécessaires à la lecture, puisque le
code du poste est imprimé dans chaque bloc.
- Pas de moteur PDF côté serveur : le navigateur sait paginer et enregistrer en
PDF. Un second moteur de rendu serait un second endroit où la grille peut
diverger de ce qui est à l'écran.
Accessibilité
- La grille porte enfin ses rôles ARIA (`grid`, `row`, `columnheader`,
`rowheader`, `gridcell`). Elle est faite de div pour la mise en page, mais se
lit comme un tableau : un lecteur d'écran doit pouvoir annoncer « ligne
Camille Ferrand, colonne mercredi ».
Cette structure a d'ailleurs révélé un test faux : `locator('div')` remontait au
conteneur de la grille, et le créneau du test atterrissait sur le premier
salarié de l'équipe au lieu de celui visé — une erreur invisible à l'écran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
90 lines
2.6 KiB
TypeScript
90 lines
2.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
daysInMonth,
|
||
formatMonthParam,
|
||
isoDayOfWeek,
|
||
monthDates,
|
||
monthLabel,
|
||
monthOf,
|
||
nextMonth,
|
||
parseMonthParam,
|
||
previousMonth,
|
||
} from '@/domain/planning/month';
|
||
|
||
describe('daysInMonth', () => {
|
||
it('suit le calendrier, y compris les années bissextiles', () => {
|
||
expect(daysInMonth({ year: 2026, month: 2 })).toBe(28);
|
||
expect(daysInMonth({ year: 2028, month: 2 })).toBe(29);
|
||
expect(daysInMonth({ year: 2026, month: 4 })).toBe(30);
|
||
expect(daysInMonth({ year: 2026, month: 12 })).toBe(31);
|
||
});
|
||
|
||
it('traite 2100 comme non bissextile', () => {
|
||
// Divisible par 4 mais pas par 400 : le piège classique d'une table écrite
|
||
// à la main.
|
||
expect(daysInMonth({ year: 2100, month: 2 })).toBe(28);
|
||
expect(daysInMonth({ year: 2000, month: 2 })).toBe(29);
|
||
});
|
||
});
|
||
|
||
describe('monthDates', () => {
|
||
it('énumère le mois entier', () => {
|
||
const dates = monthDates({ year: 2026, month: 2 });
|
||
expect(dates).toHaveLength(28);
|
||
expect(dates[0]).toBe('2026-02-01');
|
||
expect(dates[27]).toBe('2026-02-28');
|
||
});
|
||
});
|
||
|
||
describe('navigation', () => {
|
||
it('franchit l’année dans les deux sens', () => {
|
||
expect(previousMonth({ year: 2026, month: 1 })).toEqual({
|
||
year: 2025,
|
||
month: 12,
|
||
});
|
||
expect(nextMonth({ year: 2026, month: 12 })).toEqual({
|
||
year: 2027,
|
||
month: 1,
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('isoDayOfWeek', () => {
|
||
it('numérote lundi 1 et dimanche 7', () => {
|
||
// Le samedi et le dimanche portent un fond distinct dans la vue mois :
|
||
// une erreur de numérotation les décalerait d'un jour toute l'année.
|
||
expect(isoDayOfWeek('2026-08-10')).toBe(1);
|
||
expect(isoDayOfWeek('2026-08-15')).toBe(6);
|
||
expect(isoDayOfWeek('2026-08-16')).toBe(7);
|
||
});
|
||
});
|
||
|
||
describe('paramètre d’URL', () => {
|
||
it('fait un aller-retour', () => {
|
||
const month = { year: 2026, month: 8 };
|
||
expect(formatMonthParam(month)).toBe('2026-08');
|
||
expect(parseMonthParam('2026-08')).toEqual(month);
|
||
});
|
||
|
||
it('rejette ce qui n’est pas un mois', () => {
|
||
expect(parseMonthParam('2026-13')).toBeNull();
|
||
expect(parseMonthParam('2026-00')).toBeNull();
|
||
expect(parseMonthParam('août')).toBeNull();
|
||
expect(parseMonthParam(undefined)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('libellés', () => {
|
||
it('nomme le mois en français', () => {
|
||
expect(monthLabel({ year: 2026, month: 8 })).toBe('août 2026');
|
||
});
|
||
|
||
it('déduit le mois d’une date', () => {
|
||
expect(monthOf(new Date('2026-08-12T00:00:00Z'))).toEqual({
|
||
year: 2026,
|
||
month: 8,
|
||
});
|
||
});
|
||
});
|