111 lines
4.0 KiB
JavaScript
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('');
|
|
});
|
|
});
|