Files
pardis/src/lib/resume.test.js
2026-09-19 21:53:10 +02:00

111 lines
4.0 KiB
JavaScript

import { describe, expect, it } from 'vitest';
import { buildResume, resumeToText } from './resume.js';
import { atLocalTime, MINUTE } from './time.js';
const DATE = '2026-09-04';
const at = (hhmm) => atLocalTime(DATE, hhmm);
const DAY = {
date: DATE,
status: 'closed',
lunchType: 'restaurant',
restaurant: 'Le Relais',
startedTs: at('07:30'),
endedTs: at('17:30'),
};
const ENTRIES = [
{ id: 1, date: DATE, seq: 0, kind: 'travel', task: 'Chargement / transport', client: 'Dupont SA', startTs: at('07:30'), endTs: at('08:15') },
{ id: 2, date: DATE, seq: 1, kind: 'work', task: 'Terrassement', client: 'Dupont SA', startTs: at('08:15'), endTs: at('12:00') },
{ id: 3, date: DATE, seq: 2, kind: 'lunch', task: 'Pause déjeuner', client: null, startTs: at('12:00'), endTs: at('13:00') },
{ id: 4, date: DATE, seq: 3, kind: 'work', task: 'Terrassement', client: 'Dupont SA', startTs: at('13:00'), endTs: at('16:45') },
{ id: 5, date: DATE, seq: 4, kind: 'work', task: 'Retour / déchargement', client: 'Dupont SA', startTs: at('16:45'), endTs: at('17:30') },
];
describe('buildResume', () => {
it('groups by client and sums repeated tasks', () => {
const r = buildResume(DAY, ENTRIES, 15);
expect(r.clients).toHaveLength(1);
const [dupont] = r.clients;
expect(dupont.name).toBe('Dupont SA');
// Terrassement appears twice, before and after lunch, and reads as one line.
expect(dupont.tasks.map((t) => t.name)).toEqual([
'Terrassement',
'Chargement / transport',
'Retour / déchargement',
]);
expect(dupont.tasks[0].ms).toBe(450 * MINUTE); // 3h45 + 3h45
});
it('keeps lunch out of the worked total', () => {
const r = buildResume(DAY, ENTRIES, 15);
expect(r.workedMs).toBe(9 * 60 * MINUTE); // 09h00 on site, lunch excluded
expect(r.totalMs).toBe(10 * 60 * MINUTE);
expect(r.lunch).toEqual({ type: 'restaurant', restaurant: 'Le Relais', ms: 60 * MINUTE });
});
it('splits totals across several clients', () => {
const mixed = [
ENTRIES[0],
{ ...ENTRIES[1], client: 'Martin & Fils' },
...ENTRIES.slice(2),
];
const r = buildResume(DAY, mixed, 15);
expect(r.clients.map((c) => c.name)).toEqual(['Dupont SA', 'Martin & Fils']);
expect(r.workedMs).toBe(9 * 60 * MINUTE);
});
it('omits entries shorter than one rounding step, and says so', () => {
const withBlip = [
...ENTRIES,
{ id: 6, date: DATE, seq: 5, kind: 'work', task: 'Mauvais appui', client: 'Dupont SA', startTs: at('17:30'), endTs: at('17:32') },
];
const r = buildResume(DAY, withBlip, 15);
expect(r.omitted).toBe(1);
expect(r.clients[0].tasks.map((t) => t.name)).not.toContain('Mauvais appui');
});
it('keeps a 2-minute entry when rounding is set to 1 minute', () => {
const withBlip = [
...ENTRIES,
{ id: 6, date: DATE, seq: 5, kind: 'work', task: 'Mauvais appui', client: 'Dupont SA', startTs: at('17:30'), endTs: at('17:32') },
];
const r = buildResume(DAY, withBlip, 1);
expect(r.omitted).toBe(0);
expect(r.clients[0].tasks.map((t) => t.name)).toContain('Mauvais appui');
});
it('returns null without a day', () => {
expect(buildResume(null, [], 15)).toBeNull();
});
});
describe('resumeToText', () => {
it('renders the approved logbook format', () => {
expect(resumeToText(buildResume(DAY, ENTRIES, 15))).toBe(
[
'Vendredi 4 septembre',
'',
'Dupont SA — 9h00',
' Terrassement 7h30',
' Chargement / transport 0h45',
' Retour / déchargement 0h45',
'',
'Déjeuner : restaurant (Le Relais), 1h00',
'',
'Travail hors repas : 9h00',
'Total journée (repas compris) : 10h00',
].join('\n'),
);
});
it('names the panier case without a restaurant', () => {
const r = buildResume({ ...DAY, lunchType: 'panier', restaurant: null }, ENTRIES, 15);
expect(resumeToText(r)).toContain('Déjeuner : panier, 1h00');
});
it('is empty for a null resume', () => {
expect(resumeToText(null)).toBe('');
});
});