import test from 'node:test'; import assert from 'node:assert/strict'; import { buildWeeklyPdfPages, validateExportPeriod } from '../src/lib/weeklyExport.js'; import { createWeeklyPdfBytes } from '../src/lib/weeklyPdf.js'; const planning = { days: [ { date: '2026-10-05', schedule_mode: 'precise', label: 'Habituel', kind: 'work' }, { date: '2026-10-06', schedule_mode: 'precise', label: 'Repos', kind: 'rest' }, { date: '2026-10-07', schedule_mode: 'daily_hours', target_minutes: 420, label: 'Heures à effectuer', kind: 'work' }, { date: '2026-10-08', schedule_mode: 'precise', label: 'Absence', kind: 'absence' }, ], segments: [ { date: '2026-10-05', start_time: '08:00', end_time: '12:00', label: 'Habituel' }, { date: '2026-10-05', start_time: '13:00', end_time: '17:00', label: 'Habituel' }, ], }; const attendances = [ { start_date: '2026-10-05', end_date: '2026-10-05', start_time: '08:03:00', end_time: '12:00:00', duration: 3.95 }, { start_date: '2026-10-05', end_date: '2026-10-05', start_time: '13:00:00', end_time: '17:00:00', duration: 4 }, { start_date: '2026-10-06', end_date: '2026-10-06', start_time: '08:00:00', end_time: null, duration: null }, ]; const base = { start: '2026-10-05', end: '2026-10-07', name: 'Émilie', withPlanning: true, withPointage: true, planning, attendances }; const pdfString = (pages) => Buffer.from(createWeeklyPdfBytes(pages, new Date('2026-10-10T12:00:00Z'))).toString('latin1'); test('vérifie les bornes et la limite API de 366 jours', () => { assert.equal(validateExportPeriod('2026-10-05', '2026-10-11'), ''); assert.match(validateExportPeriod('2026-10-11', '2026-10-05'), /postérieure/); assert.match(validateExportPeriod('2026-02-30', '2026-03-01'), /valides/); assert.match(validateExportPeriod('2026-01-01', '2027-01-02'), /366 jours/); assert.equal(validateExportPeriod('2024-01-01', '2024-12-31'), ''); assert.throws(() => buildWeeklyPdfPages({ ...base, start: '2026-13-01' }), /date/); assert.throws(() => buildWeeklyPdfPages({ ...base, withPlanning: false, withPointage: false }), /élément/); }); test('export combiné : heures, objectifs, pointages et pointage incomplet', () => { const [page] = buildWeeklyPdfPages({ ...base, end: '2026-10-08' }); assert.equal(page.days.length, 7); assert.equal(page.weekStart, '2026-10-05'); assert.equal(page.weekEnd, '2026-10-11'); assert.equal(page.plannedMinutes, 900); // 8h précises + 7h journalières assert.equal(page.pointedMinutes, 477); assert.equal(page.incompleteCount, 1); assert.deepEqual(page.days[0].planLines, ['08:00 - 12:00', '13:00 - 17:00']); assert.match(page.days[2].planLines[0], /Heures à effectuer/); assert.match(page.days[3].planLines[0], /Absence/); assert.match(page.days[1].clockLines[0], /incomplet/); }); test('1 page par semaine, y compris quand les dates ne sont pas des lundis et dimanches', () => { const pages = buildWeeklyPdfPages({ ...base, start: '2026-10-07', end: '2026-10-22' }); assert.equal(pages.length, 3); assert.deepEqual(pages.map((page) => page.weekStart), ['2026-10-05', '2026-10-12', '2026-10-19']); assert.equal(pages[0].includedDays, 5); assert.equal(pages[1].includedDays, 7); assert.equal(pages[2].includedDays, 4); assert.equal(pages[0].days[0].included, false); assert.equal(pages[0].days[1].included, false); assert.equal(pages[2].days[4].included, false); assert.equal(pages[0].plannedMinutes, 420); // le 5 octobre est hors période assert.equal(pages[0].pointedMinutes, 0); }); test('choix planning seul ou pointages seuls ne mixe pas les données', () => { const [onlyPlan] = buildWeeklyPdfPages({ ...base, withPointage: false }); const [onlyClock] = buildWeeklyPdfPages({ ...base, withPlanning: false }); assert.equal(onlyPlan.pointedMinutes, 0); assert.equal(onlyClock.plannedMinutes, 0); assert.equal(onlyClock.pointedMinutes, 477); assert.equal(onlyPlan.days[0].clockLines.length, 0); assert.equal(onlyClock.days[0].planLines.length, 0); assert.match(pdfString([onlyPlan]), /PLANNING HEBDOMADAIRE/); assert.match(pdfString([onlyClock]), /POINTAGES HEBDOMADAIRES/); }); test('objectif hebdomadaire calculé une seule fois par semaine (même partielle)', () => { const days = ['2026-10-05', '2026-10-06', '2026-10-07'].map((date) => ({ date, schedule_mode: 'weekly_hours', week_target_minutes: 2100, kind: 'weekly_target', })); const [page] = buildWeeklyPdfPages({ ...base, start: '2026-10-06', withPointage: false, planning: { days, segments: [] } }); assert.equal(page.weeklyTarget, 2100); assert.equal(page.plannedMinutes, 2100); assert.equal(page.days[0].included, false); assert.match(pdfString([page]), /objectif hebdomadaire concerne/); }); test('pointage nocturne avec fin le lendemain et durée transmise', () => { const [page] = buildWeeklyPdfPages({ ...base, withPlanning: false, attendances: [{ start_date: '2026-10-05', end_date: '2026-10-06', start_time: '22:00:00', end_time: '03:00:00', duration: 5 }] }); assert.equal(page.pointedMinutes, 300); assert.equal(page.days[0].pointedMinutes, 300); assert.match(page.days[0].clockLines[0], /\(\+1j\)/); assert.equal(page.days[1].pointedMinutes, null); }); test('PDF A4 paysage valide : vrai PDF vectoriel, 1 page par semaine, texte français', () => { const pages = buildWeeklyPdfPages({ ...base, start: '2026-10-07', end: '2026-10-19' }); const bytes = createWeeklyPdfBytes(pages); const pdf = Buffer.from(bytes).toString('latin1'); assert.ok(bytes.length > 4000); assert.match(pdf, /^%PDF-1\.4/); assert.equal((pdf.match(/\/Type \/Page \/Parent/g) || []).length, 3); assert.match(pdf, /\/MediaBox \[0 0 841\.89 595\.28\]/); assert.match(pdf, /\/Count 3/); assert.match(pdf, /PLANNING & POINTAGES/); assert.match(pdf, /-7h00/); // écart négatif affiché sans caractère non pris en charge assert.match(pdf, /\/Encoding \/WinAnsiEncoding/); assert.match(pdf, /Page 3 \/ 3/); });